"""Фейковые клиенты MOEX/ЦБ для тестов сервисов (без сети).""" from __future__ import annotations from datetime import date from app.integrations.cbr import FxRate from app.integrations.gateway import RequestRecord from app.integrations.moex import MarketQuote, SecurityInfo, SecuritySearchHit def search_hit(secid: str, isin: str) -> SecuritySearchHit: return SecuritySearchHit( secid=secid, isin=isin, short_name=None, name=None, is_traded=True, primary_board=None, ) class FakeGateway: """Копит записи журнала, как GatewayClient: одна запись на вызов клиента.""" def __init__(self, source: str) -> None: self.source = source self.records: list[RequestRecord] = [] def record(self, endpoint: str, ok: bool = True, error: str | None = None) -> None: self.records.append( RequestRecord( source=self.source, endpoint=endpoint, params={}, http_status=200 if ok else None, duration_ms=1, ok=ok, error=error, ) ) def drain_records(self) -> list[RequestRecord]: records, self.records = self.records, [] return records class FakeMoex: """search: isin -> hit; securities: secid -> info; history: secid -> quote | Exception.""" def __init__( self, *, search: dict[str, SecuritySearchHit] | None = None, securities: dict[str, SecurityInfo] | None = None, history: dict[str, MarketQuote | Exception | None] | None = None, ) -> None: self.gateway = FakeGateway("moex") self._search = search or {} self._securities = securities or {} self._history = history or {} self.calls: list[tuple] = [] def _maybe_raise(self, value): # noqa: ANN001 if isinstance(value, Exception): raise value return value def find_by_isin(self, isin: str) -> SecuritySearchHit | None: self.calls.append(("find_by_isin", isin)) self.gateway.record("/iss/securities.json") return self._maybe_raise(self._search.get(isin)) def get_security(self, secid: str) -> SecurityInfo | None: self.calls.append(("get_security", secid)) self.gateway.record(f"/iss/securities/{secid}.json") return self._maybe_raise(self._securities.get(secid)) def get_history(self, secid: str, on_date: date) -> MarketQuote | None: self.calls.append(("get_history", secid, on_date)) value = self._history.get(secid) self.gateway.record( f"/iss/history/.../{secid}.json", ok=not isinstance(value, Exception) ) return self._maybe_raise(value) class FakeCbr: def __init__(self, rates: list[FxRate] | Exception) -> None: self.gateway = FakeGateway("cbr") self._rates = rates self.calls: list[tuple] = [] def get_rates(self, date_from: date, date_to: date, code: str = "R01375"): self.calls.append((date_from, date_to, code)) self.gateway.record("/scripts/XML_dynamic.asp") if isinstance(self._rates, Exception): raise self._rates return [r for r in self._rates if date_from <= r.rate_date <= date_to]