migration: preserve market data and search slice

This commit is contained in:
leefer
2026-07-31 01:03:43 +08:00
parent 4002f096f4
commit a4264326bd
26 changed files with 5035 additions and 4634 deletions
+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()