fix: scope dashboard snapshot failures

This commit is contained in:
ZhuLinsen
2026-08-30 16:54:57 +08:00
parent fc49613608
commit ca36ab561d
2 changed files with 118 additions and 19 deletions

View File

@@ -3,8 +3,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import date, datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Set, Tuple from typing import Any, Dict, List, Optional, Tuple
from src.config import Config, get_config from src.config import Config, get_config
from src.repositories.portfolio_repo import PortfolioRepository from src.repositories.portfolio_repo import PortfolioRepository
@@ -61,7 +61,9 @@ class DashboardOverviewService:
str, str,
Dict[str, Optional[Dict[str, Any]]], Dict[str, Optional[Dict[str, Any]]],
] = {} ] = {}
regions_with_unknown_trade_date: Set[str] = set() snapshot_candidate_ranks_by_region: Dict[str, Dict[str, int]] = {}
global_detail_failure_ranks: List[int] = []
unknown_trade_date_failure_ranks_by_region: Dict[str, List[int]] = {}
latest_reviews: List[Dict[str, Any]] = [] latest_reviews: List[Dict[str, Any]] = []
invalid_snapshot_count = 0 invalid_snapshot_count = 0
detail_failure_count = 0 detail_failure_count = 0
@@ -83,47 +85,58 @@ class DashboardOverviewService:
review_count = int(result.get("total") or 0) review_count = int(result.get("total") or 0)
if len(latest_reviews) < 5: if len(latest_reviews) < 5:
latest_reviews.extend(reviews[: 5 - len(latest_reviews)]) latest_reviews.extend(reviews[: 5 - len(latest_reviews)])
for review in reviews: for review_index, review in enumerate(reviews):
review_rank = scanned_count + review_index
context_snapshot = review.get("context_snapshot") context_snapshot = review.get("context_snapshot")
if not isinstance(context_snapshot, dict): if not isinstance(context_snapshot, dict):
detail_failure_count += 1 detail_failure_count += 1
global_detail_failure_ranks.append(review_rank)
continue continue
snapshot_container = context_snapshot.get("market_light_snapshots") snapshot_container = context_snapshot.get("market_light_snapshots")
if snapshot_container is not None and not isinstance(snapshot_container, dict): if snapshot_container is not None and not isinstance(snapshot_container, dict):
detail_failure_count += 1 detail_failure_count += 1
global_detail_failure_ranks.append(review_rank)
continue continue
for raw_region, raw_snapshot in (snapshot_container or {}).items(): for raw_region, raw_snapshot in (snapshot_container or {}).items():
region = str(raw_region).strip().lower() region = str(raw_region).strip().lower()
if region and not isinstance(raw_snapshot, dict): if region and not isinstance(raw_snapshot, dict):
invalid_snapshot_count += 1 invalid_snapshot_count += 1
regions_with_unknown_trade_date.add(region) unknown_trade_date_failure_ranks_by_region.setdefault(region, []).append(
review_rank
)
raw_snapshots = self._extract_snapshots(context_snapshot) raw_snapshots = self._extract_snapshots(context_snapshot)
for region, raw_snapshot in raw_snapshots.items(): for region, raw_snapshot in raw_snapshots.items():
raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip() raw_trade_date = str(raw_snapshot.get("trade_date") or "").strip()
try: try:
canonical_trade_date = date.fromisoformat(raw_trade_date).isoformat() canonical_trade_date = self._canonical_trade_date(raw_trade_date)
except Exception: except Exception:
invalid_snapshot_count += 1 invalid_snapshot_count += 1
regions_with_unknown_trade_date.add(region) unknown_trade_date_failure_ranks_by_region.setdefault(region, []).append(
review_rank
)
continue continue
region_ranks = snapshot_candidate_ranks_by_region.setdefault(region, {})
try: try:
snapshot = MarketLightSnapshot.model_validate(raw_snapshot).model_dump() snapshot = MarketLightSnapshot.model_validate(raw_snapshot).model_dump()
except Exception: except Exception:
invalid_snapshot_count += 1 invalid_snapshot_count += 1
snapshot_candidates_by_region.setdefault(region, {}).setdefault( region_candidates = snapshot_candidates_by_region.setdefault(region, {})
canonical_trade_date, if canonical_trade_date not in region_candidates:
None, region_candidates[canonical_trade_date] = None
) region_ranks[canonical_trade_date] = review_rank
continue continue
snapshot_region = str(snapshot.get("region") or "").strip().lower() snapshot_region = str(snapshot.get("region") or "").strip().lower()
region_candidates = snapshot_candidates_by_region.setdefault(region, {}) region_candidates = snapshot_candidates_by_region.setdefault(region, {})
if snapshot_region != region: if snapshot_region != region:
invalid_snapshot_count += 1 invalid_snapshot_count += 1
region_candidates.setdefault(canonical_trade_date, None) if canonical_trade_date not in region_candidates:
region_candidates[canonical_trade_date] = None
region_ranks[canonical_trade_date] = review_rank
continue continue
snapshot["trade_date"] = canonical_trade_date snapshot["trade_date"] = canonical_trade_date
if region_candidates.get(canonical_trade_date) is None: if region_candidates.get(canonical_trade_date) is None:
region_candidates[canonical_trade_date] = snapshot region_candidates[canonical_trade_date] = snapshot
region_ranks[canonical_trade_date] = review_rank
scanned_count += len(reviews) scanned_count += len(reviews)
if scanned_count >= review_count: if scanned_count >= review_count:
break break
@@ -147,17 +160,25 @@ class DashboardOverviewService:
unavailable_current_regions: List[str] = [] unavailable_current_regions: List[str] = []
for region, candidates in snapshot_candidates_by_region.items(): for region, candidates in snapshot_candidates_by_region.items():
ordered_dates = sorted(candidates, reverse=True) ordered_dates = sorted(candidates, reverse=True)
if detail_failure_count or region in regions_with_unknown_trade_date: candidate_ranks = snapshot_candidate_ranks_by_region.get(region, {})
unavailable_current_regions.append(region) failure_ranks = [
continue *global_detail_failure_ranks,
current = candidates[ordered_dates[0]] if ordered_dates else None *unknown_trade_date_failure_ranks_by_region.get(region, []),
if current is None: ]
current_date = ordered_dates[0] if ordered_dates else None
current = candidates[current_date] if current_date else None
current_rank = candidate_ranks.get(current_date, scanned_count) if current_date else scanned_count
if current is None or any(rank < current_rank for rank in failure_ranks):
unavailable_current_regions.append(region) unavailable_current_regions.append(region)
continue continue
selected = [current] selected = [current]
if len(ordered_dates) > 1: if len(ordered_dates) > 1:
previous = candidates[ordered_dates[1]] previous_date = ordered_dates[1]
if previous is not None: previous = candidates[previous_date]
previous_rank = candidate_ranks.get(previous_date, scanned_count)
if previous is not None and not any(
rank < previous_rank for rank in failure_ranks
):
selected.append(previous) selected.append(previous)
snapshots_by_region[region] = selected snapshots_by_region[region] = selected
@@ -203,6 +224,14 @@ class DashboardOverviewService:
source_limitations=limitations, source_limitations=limitations,
) )
@staticmethod
def _canonical_trade_date(raw_trade_date: str) -> str:
"""Parse supported ISO date forms consistently on Python 3.10+."""
raw = str(raw_trade_date or "").strip()
compact = len(raw) == 8 and raw.isascii() and raw.isdigit()
date_format = "%Y%m%d" if compact else "%Y-%m-%d"
return datetime.strptime(raw, date_format).date().isoformat()
def _personal_block(self) -> Dict[str, Any]: def _personal_block(self) -> Dict[str, Any]:
data = { data = {
"watchlist_count": None, "watchlist_count": None,

View File

@@ -384,6 +384,76 @@ def test_malformed_nested_snapshot_container_does_not_promote_older_snapshots()
assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"] assert "latest_completed_snapshot_unavailable" in payload["market"]["meta"]["limitations"]
def test_older_malformed_detail_does_not_hide_valid_current_and_baseline() -> None:
dependencies = _dependencies()
reviews = [
_review(1, "2026-08-29T09:00:00+08:00"),
_review(2, "2026-08-28T09:00:00+08:00"),
_review(3, "2026-08-27T09:00:00+08:00"),
]
dependencies["history_service"].get_history_list.side_effect = lambda **kwargs: (
_history_page(dependencies["history_service"], reviews, kwargs)
if kwargs.get("report_type") == "market_review"
else {"items": [], "total": 0}
)
def detail(record_id: int) -> dict:
if record_id == 3:
return {"context_snapshot": "{invalid-json"}
trade_date = "2026-08-29" if record_id == 1 else "2026-08-28"
return {
"context_snapshot": {
"market_light_snapshots": {
"cn": _snapshot("cn", trade_date, 60 if record_id == 1 else 50, "yellow")
}
}
}
dependencies["history_service"].get_history_detail_by_id.side_effect = detail
payload = DashboardOverviewService(**dependencies).get_overview()
assert payload["market"]["data"]["latest_snapshots"]["cn"]["trade_date"] == "2026-08-29"
assert payload["what_changed"]["data"]["current_trade_dates"] == {"cn": "2026-08-29"}
assert payload["what_changed"]["data"]["previous_trade_dates"] == {"cn": "2026-08-28"}
assert "market_review_detail_partial" in payload["market"]["meta"]["limitations"]
def test_malformed_detail_between_candidates_blocks_only_the_baseline() -> None:
dependencies = _dependencies()
reviews = [
_review(1, "2026-08-29T09:00:00+08:00"),
_review(2, "2026-08-28T09:00:00+08:00"),
_review(3, "2026-08-27T09:00:00+08:00"),
]
dependencies["history_service"].get_history_list.side_effect = lambda **kwargs: (
_history_page(dependencies["history_service"], reviews, kwargs)
if kwargs.get("report_type") == "market_review"
else {"items": [], "total": 0}
)
def detail(record_id: int) -> dict:
if record_id == 2:
return {"context_snapshot": "{invalid-json"}
trade_date = "2026-08-29" if record_id == 1 else "2026-08-27"
return {
"context_snapshot": {
"market_light_snapshots": {
"cn": _snapshot("cn", trade_date, 60 if record_id == 1 else 40, "yellow")
}
}
}
dependencies["history_service"].get_history_detail_by_id.side_effect = detail
payload = DashboardOverviewService(**dependencies).get_overview()
assert payload["market"]["data"]["latest_snapshots"]["cn"]["trade_date"] == "2026-08-29"
assert payload["what_changed"]["data"]["current_trade_dates"] == {"cn": "2026-08-29"}
assert payload["what_changed"]["data"]["previous_trade_dates"] == {}
assert "previous_completed_snapshot_unavailable:cn" in payload["what_changed"]["meta"]["limitations"]
def test_malformed_region_snapshot_does_not_promote_older_region_snapshot() -> None: def test_malformed_region_snapshot_does_not_promote_older_region_snapshot() -> None:
dependencies = _dependencies() dependencies = _dependencies()
original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect original_detail = dependencies["history_service"].get_history_detail_by_id.side_effect