fix(HEL-494): 网站市场客户端改为纯中枢 Facade,并迁移 iFinD 凭据到中枢

生产 gateway 不再读取 Tushare token 或实例化 TushareProvider/TushareClient;问财凭据经带鉴权的中枢接口加密入库,避免发版后 iFinD 未配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 23:37:53 +08:00
co-authored by Cursor multica-agent
parent 100752f43c
commit 8a7d1f3698
15 changed files with 235 additions and 83 deletions
+9 -7
View File
@@ -35,21 +35,23 @@ class DataGatewayTests(unittest.TestCase):
with self.assertRaises(DataPolicyError):
policy.assert_allowed("market.level2", "unresolved", "display")
def test_gateway_uses_live_token_supplier_and_hub_proxies(self) -> None:
token = {"value": "first"}
def test_gateway_uses_hub_facade_and_proxies(self) -> None:
gateway = build_data_gateway(
{"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
lambda: token["value"],
lambda: "must-not-be-used",
)
self.assertEqual(gateway.tushare().token, "first")
token["value"] = "second"
self.assertEqual(gateway.tushare().token, "second")
client = gateway.tushare()
self.assertEqual(client.token, "datahub")
self.assertIsNone(client.realtime_aggregator)
self.assertFalse(hasattr(client, "_legacy"))
self.assertIs(gateway.ifind, gateway.ifind_provider.client)
self.assertIs(gateway.chart_data.datahub, gateway.datahub)
self.assertIsNone(gateway.chart_data.ifind)
from backend.data.datahub.bridge import DatahubAwareTushareClient
from backend.data.datahub.ifind_proxy import HubIfindProxy
from backend.data.datahub.realtime_proxy import HubRealtimeProxy
self.assertIsInstance(client, DatahubAwareTushareClient)
self.assertIsInstance(gateway.ifind, HubIfindProxy)
self.assertIsInstance(gateway.realtime_observer, HubRealtimeProxy)
@@ -70,7 +72,6 @@ class DataGatewayTests(unittest.TestCase):
"IfindProvider": {"backend/data/gateway.py"},
"MarketChartClient": {"backend/data/gateway.py"},
"TushareClient": {"backend/features/market/service.py"},
"TushareProvider": {"backend/data/gateway.py"},
"DatahubClient": {"backend/data/gateway.py"},
"DatahubAwareTushareClient": {"backend/data/gateway.py"},
"DatahubBridge": {"backend/data/gateway.py"},
@@ -82,6 +83,7 @@ class DataGatewayTests(unittest.TestCase):
"IfindHttpClient": set(),
"EastmoneyChartClient": set(),
"WebRealtimeAggregator": set(),
"TushareProvider": set(),
}
found_forbidden = {name: set() for name in forbidden}
for path in (root / "backend").rglob("*.py"):
+9 -10
View File
@@ -546,7 +546,7 @@ class DatahubBridgeTests(unittest.TestCase):
self.assertEqual(chart[-1]["trade_date"], "2024-09-02")
self.assertEqual(chart[-1]["close"], 10.4)
def test_gateway_tushare_assembly_binds_hooks_on_inner_client(self) -> None:
def test_gateway_tushare_facade_has_no_legacy_client(self) -> None:
quotes = [
{
"ts_code": f"{index:06d}.SZ",
@@ -574,21 +574,20 @@ class DatahubBridgeTests(unittest.TestCase):
)
gateway.datahub.client = hub_client
wrapped = gateway.tushare()
inner = wrapped._legacy
self.assertTrue(callable(getattr(inner, "try_market_quotes", None)))
self.assertTrue(callable(getattr(inner, "try_index_quotes", None)))
self.assertTrue(callable(getattr(inner, "record_datahub_legacy", None)))
self.assertIs(inner.query.__self__, wrapped)
self.assertEqual(inner.query.__func__, wrapped.query.__func__)
self.assertFalse(hasattr(type(inner), "try_market_quotes"))
rows = inner.try_market_quotes("20240902")
self.assertFalse(hasattr(wrapped, "_legacy"))
self.assertIsNone(getattr(type(wrapped), "__getattr__", None))
self.assertTrue(callable(getattr(type(wrapped), "try_market_quotes", None)))
self.assertTrue(callable(getattr(type(wrapped), "try_index_quotes", None)))
self.assertTrue(callable(getattr(type(wrapped), "record_datahub_legacy", None)))
self.assertTrue(callable(getattr(type(wrapped), "dashboard", None)))
rows = wrapped.try_market_quotes("20240902")
self.assertGreaterEqual(len(rows or []), 200)
self.assertIn("/v1/quotes/latest", hub_client.paths)
hub_client.response = DatahubResponse(
data=[dict(HUB_DAILY)],
meta={"stale": False, "staleness_seconds": 0, "source": "tushare:daily"},
)
daily = inner.query("daily", {"trade_date": "20240902"}, "ts_code,amount")
daily = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,amount")
self.assertEqual(daily[0]["amount"], 2000.0)
self.assertIn("/v1/bars/daily", hub_client.paths)
+72 -4
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import ast
import json
import re
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -143,6 +144,26 @@ def hub_payload(request) -> dict:
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "tencent"},
}
if path == "/v1/auction":
return {
"schema_version": 1,
"data": [
{
"ts_code": "600000.SH",
"trade_date": "20240902",
"close": 10.2,
"vol": 1000.0,
"amount": 2000.0,
}
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "datahub"},
}
if path == "/v1/credentials/ifind":
return {
"schema_version": 1,
"data": {"configured": True, "access_ready": True, "access_expires_at": ""},
"meta": {"source": "ifind"},
}
if path == "/v1/intraday/points":
return {
"schema_version": 1,
@@ -184,7 +205,7 @@ def hub_payload(request) -> dict:
],
"meta": {"source": "ifind"},
}
if api_name in {"daily", "rt_k"}:
if api_name in {"daily", "rt_k", "stk_auction"}:
return {
"schema_version": 1,
"data": [{"ts_code": "600000.SH", "trade_date": "20240902", "close": 10.2, "amount": 2000.0}],
@@ -254,10 +275,22 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
self.assertNotIn("IfindHttpClient", source)
self.assertNotIn("EastmoneyChartClient", source)
self.assertNotIn("WebRealtimeAggregator", source)
self.assertNotIn("TushareProvider", source)
self.assertIsNone(re.search(r"(?<![A-Za-z])TushareClient\(", source))
self.assertIn("HubIfindProxy", source)
self.assertIn("HubRealtimeProxy", source)
self.assertIn("legacy.realtime_aggregator = None", source)
self.assertIn("DatahubAwareTushareClient", source)
facade = (ROOT / "backend" / "data" / "datahub" / "bridge.py").read_text(encoding="utf-8")
tree = ast.parse(facade)
cls = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "DatahubAwareTushareClient"
)
methods = {item.name for item in cls.body if isinstance(item, ast.FunctionDef)}
self.assertNotIn("__getattr__", methods)
self.assertIn("query", methods)
self.assertTrue(any(base.id == "DashboardMixin" for base in cls.bases if isinstance(base, ast.Name)))
def test_production_python_does_not_embed_blocked_hosts(self) -> None:
violations = []
@@ -273,7 +306,8 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
def test_website_runtime_does_not_call_blocked_hosts_from_gateway(self) -> None:
gateway_src = (ROOT / "backend" / "data" / "gateway.py").read_text(encoding="utf-8")
self.assertIn("legacy.realtime_aggregator = None", gateway_src)
self.assertNotIn("TushareProvider", gateway_src)
self.assertIsNone(re.search(r"(?<![A-Za-z])TushareClient\(", gateway_src))
self.assertIn("DatahubAwareTushareClient", gateway_src)
def test_bridge_query_has_no_legacy_call(self) -> None:
@@ -298,13 +332,47 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
settings = _enabled_settings()
with patch("urllib.request.urlopen", blocked_urlopen):
gateway = build_data_gateway({"tushare_token": "tok"}, datahub_settings=settings)
gateway.datahub.client = DatahubClient(settings, urlopen=blocked_urlopen)
hub_client = DatahubClient(settings, urlopen=blocked_urlopen)
gateway.datahub.client = hub_client
rows = gateway.ifind.wencai("涨停")
quotes = gateway.realtime_observer.tencent_indices()
chart = gateway.chart_data.stock_daily("600000", "20240902")
market = gateway.tushare()
market_quotes = market.try_quotes(["600000.SH"])
auction = market.query("stk_auction", {"trade_date": "20240902"}, "")
gateway.ifind.set_credentials("refresh-token", "access-token")
self.assertEqual(rows[0]["涨停原因"], "重组")
self.assertEqual(len(quotes), 3)
self.assertEqual(chart[-1]["close"], 10.2)
self.assertEqual(market_quotes[0]["close"], 10.2)
self.assertEqual(auction[0]["close"], 10.2)
self.assertIsNone(market.realtime_aggregator)
self.assertEqual(market.token, "datahub")
def test_set_credentials_posts_to_hub_not_ifind(self) -> None:
seen: list[str] = []
def urlopen(request, timeout=None):
url = str(getattr(request, "full_url", None) or request)
seen.append(url)
if any(host in url for host in BLOCKED_HOSTS):
raise AssertionError(f"website opened blocked host: {url}")
return _Resp(hub_payload(request))
settings = _enabled_settings()
hub_client = DatahubClient(settings, urlopen=urlopen)
proxy = HubIfindProxy(DatahubBridge(settings, hub_client))
proxy.set_credentials("refresh-token", "access-token")
self.assertTrue(any("/v1/credentials/ifind" in url for url in seen))
self.assertFalse(any("51ifind.com" in url for url in seen))
self.assertFalse(any("quantapi" in url for url in seen))
def test_compose_passes_ifind_env_to_hub(self) -> None:
overlay = (ROOT / "compose.datahub.yaml").read_text(encoding="utf-8")
standalone = (ROOT / "xiaobai-datahub" / "compose.yaml").read_text(encoding="utf-8")
for text in (overlay, standalone):
self.assertIn('IFIND_REFRESH_TOKEN: "${IFIND_REFRESH_TOKEN:-}"', text)
self.assertIn('IFIND_ACCESS_TOKEN: "${IFIND_ACCESS_TOKEN:-}"', text)
if __name__ == "__main__":
+7 -21
View File
@@ -380,10 +380,9 @@ class RealtimeDashboardTests(unittest.TestCase):
def test_gateway_dashboard_uses_bound_market_quotes(self) -> None:
from backend.data import build_data_gateway
from backend.data.datahub.bridge import DatahubAwareTushareClient
from backend.data.datahub.client import DatahubResponse
from backend.data.datahub.settings import DATASETS, DatahubSettings, DatasetFlags
from backend.data.gateway import DataGateway
from backend.data.providers.tushare import TushareProvider
quotes = [
{
@@ -442,30 +441,17 @@ class RealtimeDashboardTests(unittest.TestCase):
datasets = {name: DatasetFlags(name) for name in DATASETS}
datasets["quotes"] = DatasetFlags("quotes", read=True, shadow=False)
settings = DatahubSettings(base_url="http://127.0.0.1:9", token="tok", datasets=datasets)
base = build_data_gateway({"tushare_token": "tok"}, datahub_settings=settings)
gateway = DataGateway(
policy=base.policy,
quality=base.quality,
tushare_provider=TushareProvider(
lambda: "tok",
client_factory=lambda token: FakeRealtimeClient(token),
),
ifind_provider=base.ifind_provider,
chart_data=base.chart_data,
realtime_observer=base.realtime_observer,
datahub=base.datahub,
)
gateway = build_data_gateway({"tushare_token": "tok"}, datahub_settings=settings)
gateway.datahub.client = QuoteHub()
wrapped = gateway.tushare()
inner = wrapped._legacy
inner.clock = lambda: datetime(2026, 7, 20, 10, 30, tzinfo=timezone(timedelta(hours=8)))
inner.realtime_aggregator = FakeFreeAggregator(fail=True)
TushareClient._realtime_reference_cache.clear()
wrapped.clock = lambda: datetime(2026, 7, 20, 10, 30, tzinfo=timezone(timedelta(hours=8)))
wrapped.realtime_aggregator = FakeFreeAggregator(fail=True)
DatahubAwareTushareClient._realtime_reference_cache.clear()
dashboard = wrapped.dashboard("20260720")
self.assertEqual(dashboard["meta"]["quote_source"], "datahub")
self.assertIn("/v1/quotes/latest", gateway.datahub.client.calls)
self.assertTrue(callable(getattr(inner, "try_market_quotes", None)))
self.assertFalse(hasattr(type(inner), "try_market_quotes"))
self.assertTrue(callable(getattr(type(wrapped), "try_market_quotes", None)))
self.assertFalse(hasattr(wrapped, "_legacy"))
if __name__ == "__main__":