migration: preserve market insights slice
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import hashlib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import market_insights
|
||||
from backend.features.market import insights as canonical_insights
|
||||
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
ORIGINAL_ROOT = APP_ROOT.parent
|
||||
|
||||
MARKET_INSIGHT_METHODS = {
|
||||
"__init__",
|
||||
"_trade_context",
|
||||
"_latest_feature_snapshot",
|
||||
"_auction_session",
|
||||
"_stock_master",
|
||||
"_expectation_label",
|
||||
"_auction_confirmation",
|
||||
"_attention_score",
|
||||
"_auction_candidates",
|
||||
"_auction_theme_evidence",
|
||||
"_auction_amount_history",
|
||||
"_ensure_auction_amount_history",
|
||||
"_with_auction_watchlist",
|
||||
"_dynamic_auction_rows",
|
||||
"auction_center",
|
||||
"_theme_directory",
|
||||
"theme_library",
|
||||
"theme_detail",
|
||||
"_parse_concepts",
|
||||
"popularity",
|
||||
"_hot_rows",
|
||||
"_normalize_hot",
|
||||
}
|
||||
MARKET_SERVICE_METHODS = {"_market_insights"}
|
||||
AUCTION_SERVICE_METHODS = {"auction_center"}
|
||||
THEME_SERVICE_METHODS = {"theme_library", "theme_detail"}
|
||||
POPULARITY_SERVICE_METHODS = {"popularity"}
|
||||
DRAGON_TIGER_SERVICE_METHODS = {
|
||||
"get_hot_money_profiles",
|
||||
"get_dragon_tiger",
|
||||
"_apply_seat_aliases",
|
||||
}
|
||||
AUCTION_REPOSITORY_METHODS = {
|
||||
"upsert_auction_factors",
|
||||
"auction_factor_dates",
|
||||
"auction_factors_for_date",
|
||||
}
|
||||
POPULARITY_REPOSITORY_METHODS = {"upsert_popularity_factors"}
|
||||
DRAGON_TIGER_REPOSITORY_METHODS = {
|
||||
"list_seat_aliases",
|
||||
"save_seat_alias",
|
||||
"upsert_lhb_institutions",
|
||||
}
|
||||
TUSHARE_METHODS = {"hot_money_profiles", "dragon_tiger"}
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class MarketInsightsSliceSourceEquivalenceTests(unittest.TestCase):
|
||||
def assert_methods_equal(
|
||||
self,
|
||||
original_path: Path,
|
||||
original_class: str,
|
||||
migrated_path: Path,
|
||||
migrated_class: str,
|
||||
names: set[str],
|
||||
) -> None:
|
||||
original = class_methods(original_path, original_class)
|
||||
migrated = class_methods(migrated_path, migrated_class)
|
||||
self.assertEqual(set(migrated), names)
|
||||
for name in sorted(names):
|
||||
self.assertEqual(migrated[name], original[name], name)
|
||||
|
||||
def test_shared_market_insight_service_is_exact_original_ast(self) -> None:
|
||||
self.assert_methods_equal(
|
||||
ORIGINAL_ROOT / "market_insights.py",
|
||||
"MarketInsightsService",
|
||||
APP_ROOT / "backend" / "features" / "market" / "insights.py",
|
||||
"MarketInsightsService",
|
||||
MARKET_INSIGHT_METHODS,
|
||||
)
|
||||
self.assertIs(market_insights.MarketInsightsService, canonical_insights.MarketInsightsService)
|
||||
|
||||
def test_dashboard_service_methods_are_exact_original_ast(self) -> None:
|
||||
original = ORIGINAL_ROOT / "server.py"
|
||||
mappings = (
|
||||
("auction/service.py", "AuctionServiceMixin", AUCTION_SERVICE_METHODS),
|
||||
("themes/service.py", "ThemeServiceMixin", THEME_SERVICE_METHODS),
|
||||
("popularity/service.py", "PopularityServiceMixin", POPULARITY_SERVICE_METHODS),
|
||||
("dragon_tiger/service.py", "DragonTigerServiceMixin", DRAGON_TIGER_SERVICE_METHODS),
|
||||
)
|
||||
for relative, class_name, names in mappings:
|
||||
with self.subTest(relative=relative):
|
||||
self.assert_methods_equal(
|
||||
original,
|
||||
"DashboardService",
|
||||
APP_ROOT / "backend" / "features" / relative,
|
||||
class_name,
|
||||
names,
|
||||
)
|
||||
original_methods = class_methods(original, "DashboardService")
|
||||
market_methods = class_methods(
|
||||
APP_ROOT / "backend" / "features" / "market" / "service.py",
|
||||
"MarketServiceMixin",
|
||||
)
|
||||
for name in MARKET_SERVICE_METHODS:
|
||||
self.assertEqual(market_methods[name], original_methods[name], name)
|
||||
|
||||
def test_repository_methods_are_exact_original_ast(self) -> None:
|
||||
original = ORIGINAL_ROOT / "database.py"
|
||||
mappings = (
|
||||
("auction/repository.py", "AuctionRepositoryMixin", AUCTION_REPOSITORY_METHODS),
|
||||
("popularity/repository.py", "PopularityRepositoryMixin", POPULARITY_REPOSITORY_METHODS),
|
||||
("dragon_tiger/repository.py", "DragonTigerRepositoryMixin", DRAGON_TIGER_REPOSITORY_METHODS),
|
||||
)
|
||||
for relative, class_name, names in mappings:
|
||||
with self.subTest(relative=relative):
|
||||
self.assert_methods_equal(
|
||||
original,
|
||||
"ReviewDatabase",
|
||||
APP_ROOT / "backend" / "features" / relative,
|
||||
class_name,
|
||||
names,
|
||||
)
|
||||
|
||||
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")
|
||||
moved_service = (
|
||||
MARKET_SERVICE_METHODS
|
||||
| AUCTION_SERVICE_METHODS
|
||||
| THEME_SERVICE_METHODS
|
||||
| POPULARITY_SERVICE_METHODS
|
||||
| DRAGON_TIGER_SERVICE_METHODS
|
||||
)
|
||||
moved_repository = (
|
||||
AUCTION_REPOSITORY_METHODS
|
||||
| POPULARITY_REPOSITORY_METHODS
|
||||
| DRAGON_TIGER_REPOSITORY_METHODS
|
||||
)
|
||||
self.assertTrue(moved_service.isdisjoint(remaining_service))
|
||||
self.assertTrue(moved_repository.isdisjoint(remaining_database))
|
||||
|
||||
def test_tushare_dragon_tiger_implementations_are_exact_original_ast(self) -> None:
|
||||
original = class_methods(ORIGINAL_ROOT / "tushare_client.py", "TushareClient")
|
||||
migrated = class_methods(
|
||||
APP_ROOT / "backend" / "data" / "providers" / "tushare_client.py",
|
||||
"TushareClient",
|
||||
)
|
||||
for name in sorted(TUSHARE_METHODS):
|
||||
self.assertEqual(migrated[name], original[name], name)
|
||||
|
||||
def test_api_and_frontend_assets_are_unchanged(self) -> None:
|
||||
for relative in (
|
||||
"config/api.config.json",
|
||||
"static/index.html",
|
||||
"static/app.js",
|
||||
"static/styles.css",
|
||||
"static/pages/auction/page.js",
|
||||
"static/pages/themes/page.js",
|
||||
"static/pages/popularity/page.js",
|
||||
"static/pages/dragon-tiger/page.js",
|
||||
):
|
||||
self.assertEqual(
|
||||
sha256(APP_ROOT / relative),
|
||||
sha256(ORIGINAL_ROOT / relative),
|
||||
relative,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user