"""Minimal, read-only source directory (HEL-543). Registers what already exists: providers, their concrete interfaces, what capability/dataset each interface serves, and whether the provider plays a primary or backup role. This module only *describes* the current adapters and datasets already wired in `datahub/hub.py`, `datahub/serving.py`, and `datahub/realtime_serve.py`; it does not add a way to configure or add a new source without code, and it never changes routing, retries, or fallback order. Every ``interfaces`` entry below is a docs-as-code mirror of a real call site, cross-referenced in comments so a reviewer can verify each row is accurate rather than aspirational: - tushare interfaces mirror ``datahub/serving.py``'s ``_official_meta``/``source=`` strings and ``datahub/steward.py``'s live/published dataset table. - eastmoney/tencent interfaces mirror the ``observability.observe(...)`` call sites added in ``datahub/realtime_serve.py`` for HEL-543. - ifind interfaces mirror ``datahub/steward.py``'s ``IFIND_APIS`` table. """ from __future__ import annotations from typing import Any CATALOG: list[dict[str, Any]] = [ { "provider": "tushare", "label": "Tushare", "role": "official_primary", "credential_key": "tushare_token", "status_source": "src_health (legacy, kept) + provider_health (unified, HEL-543)", "interfaces": [ {"interface": "trade_cal", "capability": "交易日历", "datasets": ["calendar"]}, {"interface": "stock_basic", "capability": "股票主档", "datasets": ["stocks"]}, {"interface": "daily", "capability": "个股日K", "datasets": ["daily"]}, {"interface": "adj_factor", "capability": "复权因子", "datasets": ["daily"]}, {"interface": "daily_basic", "capability": "估值", "datasets": ["valuation"]}, {"interface": "index_daily", "capability": "指数日K", "datasets": ["index_daily"]}, {"interface": "moneyflow", "capability": "资金流", "datasets": ["moneyflow"]}, {"interface": "stk_auction", "capability": "集合竞价", "datasets": ["auction"]}, {"interface": "limit_list_d", "capability": "涨跌停池", "datasets": ["limit_events"]}, {"interface": "ths_hot", "capability": "同花顺人气榜", "datasets": ["popularity"]}, {"interface": "dc_hot", "capability": "东方财富人气榜", "datasets": ["popularity"]}, {"interface": "hm_detail", "capability": "龙虎榜游资明细", "datasets": ["dragon_tiger"]}, {"interface": "ths_daily", "capability": "同花顺概念行情", "datasets": ["sector_daily"]}, {"interface": "dc_index", "capability": "东方财富板块行情", "datasets": ["sector_daily"]}, {"interface": "sw_daily", "capability": "申万行业行情", "datasets": ["sector_daily"]}, ], }, { "provider": "eastmoney", "label": "东方财富", "role": "provisional_primary", "credential_key": None, "status_source": "provider_health (unified, HEL-543)", "interfaces": [ {"interface": "indices", "capability": "指数实时报价", "datasets": ["index_quotes"]}, {"interface": "market_quotes", "capability": "全市场实时快照", "datasets": ["quotes_latest"]}, {"interface": "named_quotes", "capability": "指定个股实时报价", "datasets": ["quotes_latest"]}, {"interface": "sector_quote", "capability": "申万板块实时报价(单个)", "datasets": ["sectors_quote"]}, {"interface": "sector_quotes_batch", "capability": "申万板块批量报价(预热)", "datasets": ["sectors_quote"]}, {"interface": "limit_pool", "capability": "涨停/炸板池(盘中)", "datasets": ["limit_pool"]}, {"interface": "intraday", "capability": "分时走势", "datasets": ["intraday_points"]}, ], }, { "provider": "tencent", "label": "腾讯行情", "role": "provisional_backup", "credential_key": None, "status_source": "provider_health (unified, HEL-543)", "interfaces": [ {"interface": "indices", "capability": "指数实时报价(东财失败时备用)", "datasets": ["index_quotes"]}, { "interface": "market_quotes_fallback", "capability": "全市场快照(备用;按本地股票主档逐只请求拼接)", "datasets": ["quotes_latest"], }, {"interface": "named_quotes", "capability": "指定个股实时报价(东财失败时备用)", "datasets": ["quotes_latest"]}, ], }, { "provider": "ifind", "label": "同花顺 iFinD", "role": "licensed_optional", "credential_key": "ifind_refresh_token", "status_source": "provider_health (unified, HEL-543) + adapter.status()", "interfaces": [ {"interface": "wencai", "capability": "问财自然语言选股", "datasets": ["ifind_wencai"]}, {"interface": "snapshots", "capability": "快照", "datasets": ["ifind_snapshots"]}, {"interface": "history", "capability": "历史行情", "datasets": ["ifind_history"]}, {"interface": "realtime", "capability": "实时行情", "datasets": ["ifind_realtime"]}, {"interface": "intraday", "capability": "分时(高频)", "datasets": ["ifind_intraday"]}, ], }, { "provider": "ths", "label": "同花顺(预留)", "role": "reserved", "credential_key": None, "status_source": "adapter.probe()(占位,本阶段未接入真实数据)", "interfaces": [], }, { "provider": "xgb", "label": "选股宝(预留)", "role": "reserved", "credential_key": None, "status_source": "adapter.probe()(占位,本阶段未接入真实数据)", "interfaces": [], }, { "provider": "akshare", "label": "AKShare(预留)", "role": "reserved", "credential_key": None, "status_source": "adapter.probe()(占位,本阶段未接入真实数据)", "interfaces": [], }, ] def snapshot(db: Any, auth: Any = None) -> list[dict[str, Any]]: """Merge the static catalog with live credential/health facts. Purely read-only: never touches routing, credentials, or adapters. Any failure while enriching one entry only degrades that entry's live data; it never drops the entry or raises, so a directory read can never break on a partially-unhealthy database. """ result: list[dict[str, Any]] = [] for entry in CATALOG: item: dict[str, Any] = { "provider": entry["provider"], "label": entry.get("label", entry["provider"]), "role": entry["role"], "status_source": entry["status_source"], "interfaces": [dict(i) for i in entry.get("interfaces", [])], } cred_key = entry.get("credential_key") if cred_key: cred = None try: if auth is not None: cred = auth.credential_status(cred_key) except Exception: cred = None item["credential"] = cred or {"configured": False, "last4": "", "updated_at": ""} else: item["credential"] = {"configured": True, "last4": "", "updated_at": "", "note": "无需凭证"} health_rows: list[dict[str, Any]] = [] try: if db is not None: health_rows = db.fetchall( "SELECT interface, state, last_ok_at, last_error, last_fallback_reason, " "consec_failures, last_latency_ms, last_data_age_seconds, updated_at " "FROM provider_health WHERE provider = ? ORDER BY interface", (entry["provider"],), ) except Exception: health_rows = [] item["live_interfaces"] = health_rows result.append(item) return result