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 from backend.data.quality import DataQualityGate, QualityEvidence, QualityReport from backend.features.market.charts import MarketChartClient @dataclass(frozen=True) class DataGateway: policy: DataSourcePolicy quality: DataQualityGate 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", ) -> DatahubAwareTushareClient: if dataset_id: self.policy.assert_allowed(dataset_id, "tushare", usage) return DatahubAwareTushareClient(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: # Website Tushare tokens are not used to assemble market clients. del tushare_token_supplier 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), ifind_provider=IfindProvider(ifind), chart_data=MarketChartClient(datahub), realtime_observer=HubRealtimeProxy(datahub), datahub=datahub, )