test_bzzoiro_source_registry.py 增至 11 项: - 新增 TestImportOrderSelfHeal:用独立子进程验证两种真实导入次序 (sources 先 / bzzoiro 先)下注册表均可用。该缺陷只在真实模块初始化 时序下成立,mock 照不出来,故不 mock。 - 新增未知名字仍抛 ValueError 的契约守卫。 src/data/sources.py:补全 _load_sources 文档,说明首次装载失败属正常的 环状导入(bzzoiro 仍在初始化),失败时保持 _loaded=False 使后续 get_source()/list_sources() 调用可重试装载(自愈)。 验证: - tests/test_bzzoiro_source_registry.py:11 passed - 两种导入次序实测均输出 bzzoiro ['bzzoiro']
91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""数据源协议 + 注册表。
|
|
|
|
定义 DataSource 契约,并提供全局注册表供路由层分发。
|
|
每个比赛数据源实现该协议,注册后即可通过统一入口调度。
|
|
|
|
当前只有 bzzoiro 一个数据源(Understat / injuries 已移除),
|
|
保留协议与注册表是为了统一 ingest 调度入口的结构。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Protocol
|
|
|
|
from src.db.base import AsyncSession
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DataSource(Protocol):
|
|
"""比赛数据源契约:抓取 → 规范化 → 入库。"""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""数据源标识名(用于路由/日志)。"""
|
|
...
|
|
|
|
async def ingest(self, db: AsyncSession, **kwargs) -> dict:
|
|
"""执行完整采集流程,返回统计。"""
|
|
...
|
|
|
|
|
|
# ── 注册表 ──
|
|
_SOURCES: dict[str, DataSource] = {}
|
|
|
|
# 延迟加载标记:保证 _load_sources() 最多执行一次,重复调用廉价
|
|
_loaded = False
|
|
|
|
|
|
def register(source):
|
|
"""装饰器:将数据源注册到全局注册表。
|
|
|
|
兼容类注册与实例注册:类会被实例化后存入(保证 get_source 返回实例)。
|
|
"""
|
|
obj = source() if isinstance(source, type) else source
|
|
_SOURCES[obj.name] = obj
|
|
return source
|
|
|
|
|
|
def get_source(name: str) -> DataSource:
|
|
"""按名获取数据源。"""
|
|
_load_sources()
|
|
if name not in _SOURCES:
|
|
raise ValueError(f"未知数据源: {name}")
|
|
return _SOURCES[name]
|
|
|
|
|
|
def list_sources() -> list[str]:
|
|
"""列出所有已注册数据源名。"""
|
|
_load_sources()
|
|
return list(_SOURCES.keys())
|
|
|
|
|
|
def _load_sources() -> None:
|
|
"""延迟导入数据源触发 @register(避免循环导入)。
|
|
|
|
导入失败必须留下痕迹:静默吞掉 ImportError 会让注册表恒为空,
|
|
导致 get_source() 对所有数据源都报「未知数据源」,把导入错误
|
|
伪装成「不存在的名字」—— 这类静默失效极难定位,故在此显式记日志。
|
|
|
|
失败时保持 _loaded=False:后续调用会重新尝试导入。正常应用流程中
|
|
src.api.app 先导入本模块,此处的模块级预热是在 `src.data.bzzoiro`
|
|
尚未初始化时发起的,导入链在本模块内成环,首次必然失败(bzzoiro
|
|
仍在加载中),由后续 get_source() / list_sources() 调用完成真正的装载。
|
|
"""
|
|
global _loaded
|
|
if _loaded:
|
|
return
|
|
try:
|
|
from src.data.bzzoiro import BzzoiroSource # noqa: F401
|
|
except Exception:
|
|
logger.exception(
|
|
"数据源模块导入失败(通常为初始化中途的环状导入),本次注册表为空;"
|
|
"下次 get_source()/list_sources() 调用会自动重试"
|
|
)
|
|
return # 保持 _loaded=False,下次调用可重试
|
|
_loaded = True
|
|
|
|
|
|
# 模块加载时预热(失败会记录日志,不再静默)
|
|
_load_sources()
|