from __future__ import annotations import hashlib import importlib import inspect import json import sys from dataclasses import dataclass from pathlib import Path from typing import Any EXPECTED = { "__init__.py": (257, "F39479C1A7B7F40A1A71D5EDE8A287F4E701B4F1473D674F04A2E1BA1B522D29"), "core.py": (53165, "6AB34917A8B136F684EF655995428ACB5936050E1DA36B7B2371941A3E5A5F8F"), "cli.py": (1811, "260692D62C8983B623483E0AA29C763869E30D40952700D46C636357A7376A1E"), "source_registry.json": (4958, "0895D4F0559A1353787059D506DF16572E6818CD847C21042FE1D1106AA854E0"), } class V1ContractError(RuntimeError): pass def _sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest().upper() @dataclass(frozen=True) class V1Bridge: module: Any package_dir: Path registry_path: Path fingerprints: dict[str, dict[str, Any]] @classmethod def load(cls) -> "V1Bridge": repo = Path(__file__).resolve().parents[3] parent = repo / "dev" / "ana-dev" package = parent / "stock_valuation_pipeline" fingerprints: dict[str, dict[str, Any]] = {} for name, (size, digest) in EXPECTED.items(): path = package / name if not path.is_file() or path.stat().st_size != size or _sha(path) != digest: raise V1ContractError(f"V1 合同漂移:{path}") fingerprints[name] = {"bytes": size, "sha256": digest} parent_text = str(parent) if parent_text not in sys.path: sys.path.insert(0, parent_text) module = importlib.import_module("stock_valuation_pipeline") module_file = Path(module.__file__).resolve() if package.resolve() not in module_file.parents or module.__version__ != "1.0.0": raise V1ContractError("V1 模块加载路径或版本不匹配") expected_signatures = { "run_pipeline": "(snapshot_path: 'Path', output_dir: 'Path', source_registry_path: 'Path', *, force: 'bool' = False) -> 'dict[str, Any]'", "compute_valuation": "(snapshot: 'dict[str, Any]', registry: 'dict[str, Any]') -> 'dict[str, Any]'", } for name, expected in expected_signatures.items(): actual = str(inspect.signature(getattr(module, name))) if actual != expected: raise V1ContractError(f"V1 {name} 签名漂移:{actual}") return cls(module, package, package / "source_registry.json", fingerprints) def metadata(self) -> dict[str, Any]: return { "version": self.module.__version__, "module_path": str(Path(self.module.__file__).resolve()), "registry_path": str(self.registry_path.resolve()), "files": self.fingerprints, } def run_input(self, snapshot_path: Path, output_dir: Path, force: bool) -> dict[str, Any]: return self.module.run_pipeline( snapshot_path=snapshot_path, output_dir=output_dir, source_registry_path=self.registry_path, force=force, ) def compute(self, snapshot_path: Path) -> dict[str, Any]: snapshot = self.module.load_snapshot(snapshot_path) registry = json.loads(self.registry_path.read_text(encoding="utf-8")) return self.module.compute_valuation(snapshot, registry)