from __future__ import annotations
|
|
import copy
|
import json
|
from pathlib import Path
|
from typing import Any
|
|
|
ALLOWED_TOP = {"normalization", "valuation", "analysis"}
|
PROTECTED_KEYS = {
|
"price",
|
"price_timestamp",
|
"diluted_shares",
|
"shares_date",
|
"platform_market_cap",
|
"revenue",
|
"attributable_profit",
|
"deduct_profit",
|
"period_end",
|
"publish_date",
|
"source_id",
|
"url",
|
"raw_hash",
|
}
|
|
|
class JudgmentConflict(ValueError):
|
pass
|
|
|
def _scan(value: Any, path: str = "") -> None:
|
if isinstance(value, dict):
|
for key, child in value.items():
|
current = f"{path}.{key}" if path else key
|
reference_only = current.startswith("normalization.adjustments[") and key == "source_id"
|
if key in PROTECTED_KEYS and not reference_only:
|
raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:禁止字段 {current}")
|
_scan(child, current)
|
elif isinstance(value, list):
|
for index, child in enumerate(value):
|
_scan(child, f"{path}[{index}]")
|
|
|
def load_overlay(path: Path) -> dict[str, Any]:
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
unknown = set(payload) - ALLOWED_TOP
|
if unknown:
|
raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:未知顶层字段 {sorted(unknown)}")
|
_scan(payload)
|
scenarios = ((payload.get("valuation") or {}).get("scenarios") or [])
|
if not scenarios or sum(1 for item in scenarios if item.get("role") == "base") != 1:
|
raise JudgmentConflict("judgment 必须包含且只包含一个 base 情景")
|
return payload
|
|
|
def apply_overlay(data_snapshot: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
snapshot = copy.deepcopy(data_snapshot)
|
before = {
|
key: copy.deepcopy(snapshot.get(key))
|
for key in ("meta", "market", "financials", "balance_sheet", "institutions", "sources")
|
}
|
for key in ALLOWED_TOP:
|
if key in overlay:
|
snapshot[key] = copy.deepcopy(overlay[key])
|
known_sources = {item["id"] for item in data_snapshot.get("sources", [])}
|
for adjustment in snapshot.get("normalization", {}).get("adjustments", []):
|
source_id = adjustment.get("source_id")
|
if source_id not in known_sources:
|
raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:未知只读来源引用 {source_id}")
|
after = {key: snapshot.get(key) for key in before}
|
if before != after:
|
raise JudgmentConflict("E_JUDGMENT_CONFLICT:保护数据被覆盖")
|
return snapshot
|