from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime from typing import Any from backend.data.contracts import DataUsage from backend.data.datahub import DatahubAwareTushareClient, DatahubBridge, DatahubClient, DatahubSettings from backend.data.datahub.ifind_proxy import HubIfindProxy from backend.data.datahub.realtime_proxy import HubRealtimeProxy from backend.data.policy import DataSourcePolicy from backend.data.providers import IfindProvider, TushareProvider from backend.data.quality import DataQualityGate, QualityEvidence, QualityReport from backend.data.providers.tushare_client import TushareClient from backend.features.market.charts import MarketChartClient @dataclass(frozen=True) class DataGateway: policy: DataSourcePolicy quality: DataQualityGate tushare_provider: TushareProvider ifind_provider: IfindProvider chart_data: MarketChartClient realtime_observer: HubRealtimeProxy datahub: DatahubBridge @property def ifind(self) -> HubIfindProxy: return self.ifind_provider.client def tushare( self, dataset_id: str = "", usage: DataUsage = "calculation", ) -> TushareClient: if dataset_id: self.policy.assert_allowed(dataset_id, "tushare", usage) legacy = self.tushare_provider.client() legacy.realtime_aggregator = None return DatahubAwareTushareClient(legacy, self.datahub) def dataset_status(self, trade_date: str) -> list[dict[str, Any]] | None: return self.datahub.dataset_status(trade_date) def batches(self, trade_date: str, dataset: str = "") -> list[dict[str, Any]] | None: return self.datahub.batches(trade_date, dataset) def datahub_status(self) -> dict[str, Any]: from backend.data.datahub.route_state import DATASET_LABELS, LEDGER from backend.data.datahub.settings import DATASETS settings = self.datahub.settings flags = [] enabled = 0 for name in DATASETS: read = bool(settings.flags(name).read) if read: enabled += 1 flags.append({"dataset": name, "label": DATASET_LABELS.get(name, name), "read": read}) routes = LEDGER.snapshot() fallbacks = [item for item in routes if item.get("route") == "legacy"] return { "configured": bool(settings.token and settings.base_url), "base_url": settings.base_url, "enabled_reads": enabled, "total_reads": len(DATASETS), "flags": flags, "routes": routes, "fallback_count": len(fallbacks), "fallback_labels": [str(item.get("label") or item.get("dataset")) for item in fallbacks], } def assert_source(self, dataset_id: str, provider_id: str, usage: DataUsage) -> None: self.policy.assert_allowed(dataset_id, provider_id, usage) def provider_chain(self, dataset_id: str, usage: DataUsage) -> tuple[str, ...]: dataset = self.policy.dataset(dataset_id) allowed = [] for provider_id in dataset.providers: try: self.policy.assert_allowed(dataset_id, provider_id, usage) except Exception: continue allowed.append(provider_id) if not allowed: raise RuntimeError(f"No permitted provider for {dataset_id} ({usage})") return tuple(allowed) def require_quality( self, evidence: QualityEvidence, usage: DataUsage, as_of: str | datetime | None = None, ) -> QualityReport: return self.quality.require(evidence, usage, as_of) def build_data_gateway( credentials: dict[str, object], tushare_token_supplier: Callable[[], str] | None = None, datahub_settings: DatahubSettings | None = None, ) -> DataGateway: token_supplier = tushare_token_supplier or ( lambda: str(credentials.get("tushare_token") or "") ) policy = DataSourcePolicy.load() settings = datahub_settings or DatahubSettings.load(credentials=credentials) datahub_client = DatahubClient(settings) datahub = DatahubBridge(settings, datahub_client) ifind = HubIfindProxy(datahub) return DataGateway( policy=policy, quality=DataQualityGate.load(policy), tushare_provider=TushareProvider(token_supplier), ifind_provider=IfindProvider(ifind), chart_data=MarketChartClient(datahub), realtime_observer=HubRealtimeProxy(datahub), datahub=datahub, )