migration: preserve market data and search slice

This commit is contained in:
leefer
2026-07-31 01:03:43 +08:00
parent 982af4ba92
commit ac94f5285e
22 changed files with 4956 additions and 4628 deletions
+5 -1
View File
@@ -48,7 +48,11 @@ class DataGatewayTests(unittest.TestCase):
from pathlib import Path
source = (
Path(__file__).resolve().parents[1] / "backend" / "application.py"
Path(__file__).resolve().parents[1]
/ "backend"
/ "features"
/ "market"
/ "service.py"
).read_text(encoding="utf-8")
self.assertEqual(source.count("TushareClient(self.token)"), 1)
self.assertIn("return gateway.tushare()", source)
+159
View File
@@ -0,0 +1,159 @@
from __future__ import annotations
import ast
import hashlib
import unittest
from pathlib import Path
import chart_data_provider
import ifind_client
import realtime_aggregator
import tushare_client
from backend.data import realtime
from backend.data.providers import ifind_client as canonical_ifind
from backend.data.providers import tushare_client as canonical_tushare
from backend.features.market import charts
APP_ROOT = Path(__file__).resolve().parents[1]
ORIGINAL_ROOT = APP_ROOT.parent
MARKET_METHODS = {
"_tushare_client",
"get_dashboard",
"_dashboard_sentiment_ready",
"_display_compact_date",
"_carry_dashboard",
"_realtime_snapshot_due",
"sync_dashboard",
"realtime_aggregate_health",
"_search_market_directory",
"_search_match_score",
"search_entities",
"get_search_detail",
"get_intraday_chart",
"_ths_search_detail",
"_index_search_detail",
"get_stock_detail",
"_stock_detail_bar_date",
"_stock_detail_cache_needs_refresh",
"_prepare_stock_detail",
"_sanitize_stock_detail_prices",
"_valid_realtime_stock_quote",
"_ifind_realtime_stock_quote",
"_merge_realtime_stock_detail",
"get_stock_preview",
"backfill",
"_stock_identity",
"_enrich_stock_detail",
"_with_storage",
"_record_count",
}
MARKET_REPOSITORY_METHODS = {
"get_snapshot",
"get_latest_real_snapshot",
"save_snapshot",
"get_data_snapshot",
"get_latest_data_snapshot",
"save_data_snapshot",
"search_stock_master",
"list_snapshot_payloads",
"start_sync",
"finish_sync",
"status",
}
def class_methods(path: Path, class_name: str) -> dict[str, str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
owner = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == class_name
)
return {
node.name: ast.dump(node, include_attributes=False)
for node in owner.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def top_level_definitions(path: Path) -> dict[str, str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
return {
node.name: ast.dump(node, include_attributes=False)
for node in tree.body
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))
}
class MarketSliceSourceEquivalenceTests(unittest.TestCase):
def test_market_service_methods_are_exact_original_ast(self) -> None:
original = class_methods(ORIGINAL_ROOT / "server.py", "DashboardService")
migrated = class_methods(
APP_ROOT / "backend" / "features" / "market" / "service.py",
"MarketServiceMixin",
)
self.assertEqual(set(migrated), MARKET_METHODS)
for name in sorted(MARKET_METHODS):
self.assertEqual(migrated[name], original[name], name)
def test_market_repository_methods_are_exact_original_ast(self) -> None:
original = class_methods(ORIGINAL_ROOT / "database.py", "ReviewDatabase")
migrated = class_methods(
APP_ROOT / "backend" / "features" / "market" / "repository.py",
"MarketRepositoryMixin",
)
self.assertEqual(set(migrated), MARKET_REPOSITORY_METHODS)
for name in sorted(MARKET_REPOSITORY_METHODS):
self.assertEqual(migrated[name], original[name], name)
def test_original_classes_no_longer_duplicate_moved_methods(self) -> None:
remaining_service = class_methods(APP_ROOT / "backend" / "application.py", "DashboardService")
remaining_database = class_methods(APP_ROOT / "database.py", "ReviewDatabase")
self.assertTrue(MARKET_METHODS.isdisjoint(remaining_service))
self.assertTrue(MARKET_REPOSITORY_METHODS.isdisjoint(remaining_database))
def test_provider_compatibility_modules_are_canonical_aliases(self) -> None:
self.assertIs(tushare_client.TushareClient, canonical_tushare.TushareClient)
self.assertIs(ifind_client.IfindHttpClient, canonical_ifind.IfindHttpClient)
self.assertIs(realtime_aggregator.WebRealtimeAggregator, realtime.WebRealtimeAggregator)
self.assertIs(chart_data_provider.MarketChartClient, charts.MarketChartClient)
def test_provider_logic_is_the_original_implementation(self) -> None:
exact_moves = (
("tushare_client.py", "backend/data/providers/tushare_client.py"),
("ifind_client.py", "backend/data/providers/ifind_client.py"),
("realtime_aggregator.py", "backend/data/realtime.py"),
)
for original, migrated in exact_moves:
self.assertEqual(sha256(ORIGINAL_ROOT / original), sha256(APP_ROOT / migrated))
self.assertEqual(
top_level_definitions(ORIGINAL_ROOT / "chart_data_provider.py"),
top_level_definitions(APP_ROOT / "backend/features/market/charts.py"),
)
def test_unchanged_frontend_assets_match_the_original(self) -> None:
for relative in (
"index.html",
"app.js",
"styles.css",
"renovation.css",
"redesign-v2.css",
"theme.css",
"wentian-v2.css",
):
self.assertEqual(
sha256(APP_ROOT / "static" / relative),
sha256(ORIGINAL_ROOT / "static" / relative),
relative,
)
if __name__ == "__main__":
unittest.main()
+6 -6
View File
@@ -90,8 +90,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
"moneyflow": {},
}
with patch("backend.application.datetime", FixedMarketDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
with patch("backend.features.market.service.datetime", FixedMarketDatetime), patch(
"backend.features.market.service.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(cached, "002141", today)
@@ -112,8 +112,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
"stock": {"code": "002141", "price": 10, "change": 1.2},
"prices": [{"trade_date": historical, "close": 10, "change": 1.2}],
}
with patch("backend.application.datetime", FixedMarketDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
with patch("backend.features.market.service.datetime", FixedMarketDatetime), patch(
"backend.features.market.service.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(payload, "002141", historical)
@@ -151,8 +151,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
},
],
}
with patch("backend.application.datetime", FixedPreopenDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
with patch("backend.features.market.service.datetime", FixedPreopenDatetime), patch(
"backend.features.market.service.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(payload, "002141", today)