276 lines
8.4 KiB
Python
276 lines
8.4 KiB
Python
import random
|
|
import uuid
|
|
from datetime import timedelta
|
|
from typing import Any, Callable, Dict, Iterator, Type
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from pytest_freezegun import freeze_time
|
|
|
|
from t_tech.invest import (
|
|
Bond,
|
|
BondResponse,
|
|
BondsResponse,
|
|
Client,
|
|
CurrenciesResponse,
|
|
Currency,
|
|
CurrencyResponse,
|
|
Etf,
|
|
EtfResponse,
|
|
EtfsResponse,
|
|
Future,
|
|
FutureResponse,
|
|
FuturesResponse,
|
|
InstrumentIdType,
|
|
Share,
|
|
ShareResponse,
|
|
SharesResponse,
|
|
)
|
|
from t_tech.invest.caching.instruments_cache.instruments_cache import InstrumentsCache
|
|
from t_tech.invest.caching.instruments_cache.models import InstrumentsResponse
|
|
from t_tech.invest.caching.instruments_cache.settings import InstrumentsCacheSettings
|
|
from t_tech.invest.services import Services
|
|
|
|
|
|
def uid() -> str:
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
@pytest.fixture()
|
|
def token() -> str:
|
|
return uid()
|
|
|
|
|
|
@pytest.fixture()
|
|
def real_services(token: str) -> Iterator[Services]:
|
|
with Client(token) as services:
|
|
yield services
|
|
|
|
|
|
def gen_meta_ids() -> Dict[str, str]:
|
|
return {"class_code": uid(), "figi": uid(), "ticker": uid(), "uid": uid()}
|
|
|
|
|
|
def gen_instruments(type_: Type, instrument_count: int = 10):
|
|
return [
|
|
type_(name=f"{type_.__name__}_{i}", **gen_meta_ids())
|
|
for i in range(instrument_count)
|
|
]
|
|
|
|
|
|
def gen_instruments_response(
|
|
response_type: Type, type_: Type, instrument_count: int = 10
|
|
):
|
|
return response_type(instruments=gen_instruments(type_, instrument_count))
|
|
|
|
|
|
@pytest.fixture()
|
|
def instrument_map():
|
|
return {
|
|
Etf: gen_instruments_response(EtfsResponse, Etf),
|
|
Share: gen_instruments_response(SharesResponse, Share),
|
|
Bond: gen_instruments_response(BondsResponse, Bond),
|
|
Currency: gen_instruments_response(CurrenciesResponse, Currency),
|
|
Future: gen_instruments_response(FuturesResponse, Future),
|
|
}
|
|
|
|
|
|
def mock_get_by(instrument_response: InstrumentsResponse, response_type):
|
|
type_to_field_extractor = {
|
|
InstrumentIdType.INSTRUMENT_ID_UNSPECIFIED: lambda i: i.figi,
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI: lambda i: i.figi,
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER: lambda i: i.ticker,
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_UID: lambda i: i.uid,
|
|
}
|
|
|
|
def _mock_get_by(
|
|
*,
|
|
id_type: InstrumentIdType = InstrumentIdType(0),
|
|
class_code: str = "",
|
|
id: str = "",
|
|
):
|
|
get_id = type_to_field_extractor[id_type]
|
|
|
|
def filter_(instrument):
|
|
return get_id(instrument) == id and instrument.class_code == class_code
|
|
|
|
(found_instrument,) = filter(filter_, instrument_response.instruments)
|
|
return response_type(instrument=found_instrument)
|
|
|
|
return Mock(wraps=_mock_get_by)
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_instruments_service(
|
|
real_services: Services,
|
|
mocker,
|
|
instrument_map,
|
|
) -> Services:
|
|
real_services.instruments = mocker.Mock(
|
|
wraps=real_services.instruments,
|
|
)
|
|
|
|
real_services.instruments.etfs.__name__ = "etfs"
|
|
real_services.instruments.etfs.return_value = instrument_map[Etf]
|
|
real_services.instruments.etf_by = mock_get_by(instrument_map[Etf], EtfResponse)
|
|
|
|
real_services.instruments.shares.__name__ = "shares"
|
|
real_services.instruments.shares.return_value = instrument_map[Share]
|
|
real_services.instruments.share_by = mock_get_by(
|
|
instrument_map[Share], ShareResponse
|
|
)
|
|
|
|
real_services.instruments.bonds.__name__ = "bonds"
|
|
real_services.instruments.bonds.return_value = instrument_map[Bond]
|
|
real_services.instruments.bond_by = mock_get_by(instrument_map[Bond], BondResponse)
|
|
|
|
real_services.instruments.currencies.__name__ = "currencies"
|
|
real_services.instruments.currencies.return_value = instrument_map[Currency]
|
|
real_services.instruments.currency_by = mock_get_by(
|
|
instrument_map[Currency], CurrencyResponse
|
|
)
|
|
|
|
real_services.instruments.futures.__name__ = "futures"
|
|
real_services.instruments.futures.return_value = instrument_map[Future]
|
|
real_services.instruments.future_by = mock_get_by(
|
|
instrument_map[Future], FutureResponse
|
|
)
|
|
|
|
return real_services
|
|
|
|
|
|
@pytest.fixture()
|
|
def mocked_services(
|
|
real_services: Services,
|
|
mock_instruments_service,
|
|
) -> Services:
|
|
return real_services
|
|
|
|
|
|
@pytest.fixture()
|
|
def settings() -> InstrumentsCacheSettings:
|
|
return InstrumentsCacheSettings()
|
|
|
|
|
|
@pytest.fixture()
|
|
def frozen_datetime():
|
|
with freeze_time() as frozen_datetime:
|
|
yield frozen_datetime
|
|
|
|
|
|
@pytest.fixture()
|
|
def instruments_cache(
|
|
settings: InstrumentsCacheSettings, mocked_services, frozen_datetime
|
|
) -> InstrumentsCache:
|
|
return InstrumentsCache(
|
|
settings=settings, instruments_service=mocked_services.instruments
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("get_instruments_of_type", "get_instrument_of_type_by"),
|
|
[
|
|
(
|
|
lambda instruments: instruments.etfs,
|
|
lambda instruments: instruments.etf_by,
|
|
),
|
|
(
|
|
lambda instruments: instruments.shares,
|
|
lambda instruments: instruments.share_by,
|
|
),
|
|
(
|
|
lambda instruments: instruments.bonds,
|
|
lambda instruments: instruments.bond_by,
|
|
),
|
|
(
|
|
lambda instruments: instruments.currencies,
|
|
lambda instruments: instruments.currency_by,
|
|
),
|
|
(
|
|
lambda instruments: instruments.futures,
|
|
lambda instruments: instruments.future_by,
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("id_type", "get_id"),
|
|
[
|
|
(
|
|
InstrumentIdType.INSTRUMENT_ID_UNSPECIFIED,
|
|
lambda instrument: instrument.figi,
|
|
),
|
|
(
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_FIGI,
|
|
lambda instrument: instrument.figi,
|
|
),
|
|
(
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_TICKER,
|
|
lambda instrument: instrument.ticker,
|
|
),
|
|
(
|
|
InstrumentIdType.INSTRUMENT_ID_TYPE_UID,
|
|
lambda instrument: instrument.uid,
|
|
),
|
|
],
|
|
)
|
|
class TestInstrumentCache:
|
|
def test_gets_from_net_then_cache(
|
|
self,
|
|
mocked_services: Services,
|
|
settings: InstrumentsCacheSettings,
|
|
instruments_cache: InstrumentsCache,
|
|
get_instruments_of_type,
|
|
get_instrument_of_type_by,
|
|
id_type: InstrumentIdType,
|
|
get_id: Callable[[Any], str],
|
|
):
|
|
get_instruments = get_instruments_of_type(mocked_services.instruments)
|
|
get_instrument_by = get_instrument_of_type_by(mocked_services.instruments)
|
|
get_instrument_by_cached = get_instrument_of_type_by(instruments_cache)
|
|
(inst,) = random.sample(get_instruments().instruments, k=1)
|
|
from_server = get_instrument_by(
|
|
id_type=id_type,
|
|
class_code=inst.class_code,
|
|
id=get_id(inst),
|
|
)
|
|
get_instrument_by.assert_called_once()
|
|
get_instrument_by.reset_mock()
|
|
|
|
from_cache = get_instrument_by_cached(
|
|
id_type=id_type,
|
|
class_code=inst.class_code,
|
|
id=get_id(inst),
|
|
)
|
|
|
|
get_instrument_by.assert_not_called()
|
|
assert str(from_server) == str(from_cache)
|
|
|
|
@pytest.mark.parametrize(
|
|
"settings", [InstrumentsCacheSettings(ttl=timedelta(milliseconds=10))]
|
|
)
|
|
def test_refreshes_on_ttl(
|
|
self,
|
|
mocked_services: Services,
|
|
settings: InstrumentsCacheSettings,
|
|
instruments_cache: InstrumentsCache,
|
|
get_instruments_of_type,
|
|
get_instrument_of_type_by,
|
|
id_type: InstrumentIdType,
|
|
get_id: Callable[[Any], str],
|
|
frozen_datetime,
|
|
):
|
|
get_instruments = get_instruments_of_type(mocked_services.instruments)
|
|
get_instrument_by_cached = get_instrument_of_type_by(instruments_cache)
|
|
get_instruments.assert_called_once()
|
|
(inst,) = random.sample(get_instruments().instruments, k=1)
|
|
get_instruments.reset_mock()
|
|
frozen_datetime.tick(timedelta(seconds=10))
|
|
|
|
_ = get_instrument_by_cached(
|
|
id_type=id_type,
|
|
class_code=inst.class_code,
|
|
id=get_id(inst),
|
|
)
|
|
|
|
get_instruments.assert_called_once()
|