100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Фейковые клиенты 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]
|