build 039: complete Quotes Feed migration foundation
This commit is contained in:
463
app/tests/unit/storage/test_instrument_store.py
Normal file
463
app/tests/unit/storage/test_instrument_store.py
Normal file
@@ -0,0 +1,463 @@
|
||||
# app/tests/unit/storage/test_instrument_store.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.models.instrument import Instrument
|
||||
from src.storage.exceptions import (
|
||||
InstrumentStoreError,
|
||||
StorageError,
|
||||
)
|
||||
from src.storage.instrument_store import (
|
||||
InMemoryInstrumentStore,
|
||||
InstrumentStoreProtocol,
|
||||
)
|
||||
|
||||
|
||||
def _instrument(
|
||||
*,
|
||||
symbol: str = "BTC/USD_LEVERAGE",
|
||||
name: str = "BTC/USD",
|
||||
base_asset: str = "BTC",
|
||||
) -> Instrument:
|
||||
return Instrument(
|
||||
symbol=symbol,
|
||||
name=name,
|
||||
status="TRADING",
|
||||
base_asset=base_asset,
|
||||
quote_asset="USD",
|
||||
asset_type="CRYPTOCURRENCY",
|
||||
market_type="LEVERAGE",
|
||||
market_modes=("REGULAR",),
|
||||
order_types=("LIMIT", "MARKET", "STOP"),
|
||||
base_asset_precision=4,
|
||||
quote_asset_precision=4,
|
||||
tick_size=Decimal("0.05"),
|
||||
tick_value=Decimal("3878.86"),
|
||||
step_size=Decimal("0.0001"),
|
||||
min_qty=Decimal("0.0001"),
|
||||
max_qty=Decimal("1000"),
|
||||
min_notional=Decimal("1"),
|
||||
country=None,
|
||||
sector=None,
|
||||
industry=None,
|
||||
trading_hours=None,
|
||||
)
|
||||
|
||||
|
||||
def test_in_memory_store_matches_protocol() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
assert isinstance(
|
||||
store,
|
||||
InstrumentStoreProtocol,
|
||||
)
|
||||
|
||||
|
||||
def test_get_returns_none_for_unknown_source() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
assert store.get("dzengi") is None
|
||||
|
||||
|
||||
def test_set_and_get_preserve_same_tuple_object() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
instruments = (
|
||||
_instrument(),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
instruments,
|
||||
)
|
||||
|
||||
result = store.get("dzengi")
|
||||
|
||||
assert result is instruments
|
||||
|
||||
|
||||
def test_empty_tuple_is_distinct_from_cache_miss() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
assert store.get("dzengi") is None
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
(),
|
||||
)
|
||||
|
||||
result = store.get("dzengi")
|
||||
|
||||
assert result == ()
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_different_sources_are_isolated() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
dzengi_instruments = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
)
|
||||
secondary_instruments = (
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
dzengi_instruments,
|
||||
)
|
||||
store.set(
|
||||
"secondary",
|
||||
secondary_instruments,
|
||||
)
|
||||
|
||||
assert store.get("dzengi") is dzengi_instruments
|
||||
assert store.get("secondary") is secondary_instruments
|
||||
|
||||
|
||||
def test_clear_removes_only_requested_source() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
dzengi_instruments = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
)
|
||||
secondary_instruments = (
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
dzengi_instruments,
|
||||
)
|
||||
store.set(
|
||||
"secondary",
|
||||
secondary_instruments,
|
||||
)
|
||||
|
||||
store.clear("dzengi")
|
||||
|
||||
assert store.get("dzengi") is None
|
||||
assert store.get("secondary") is secondary_instruments
|
||||
|
||||
|
||||
def test_clear_without_source_removes_all_sources() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
(
|
||||
_instrument(),
|
||||
),
|
||||
)
|
||||
store.set(
|
||||
"secondary",
|
||||
(
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
store.clear()
|
||||
|
||||
assert store.get("dzengi") is None
|
||||
assert store.get("secondary") is None
|
||||
|
||||
|
||||
def test_set_replaces_existing_value() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
first = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
)
|
||||
second = (
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
first,
|
||||
)
|
||||
store.set(
|
||||
"dzengi",
|
||||
second,
|
||||
)
|
||||
|
||||
assert store.get("dzengi") is second
|
||||
|
||||
|
||||
def test_source_name_outer_spaces_are_normalized() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
instruments = (
|
||||
_instrument(),
|
||||
)
|
||||
|
||||
store.set(
|
||||
" dzengi ",
|
||||
instruments,
|
||||
)
|
||||
|
||||
assert store.get("dzengi") is instruments
|
||||
assert store.get(" dzengi ") is instruments
|
||||
|
||||
|
||||
def test_source_name_case_is_preserved() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
lowercase = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
)
|
||||
uppercase = (
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
lowercase,
|
||||
)
|
||||
store.set(
|
||||
"DZENGI",
|
||||
uppercase,
|
||||
)
|
||||
|
||||
assert store.get("dzengi") is lowercase
|
||||
assert store.get("DZENGI") is uppercase
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_name",
|
||||
[
|
||||
"",
|
||||
" ",
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
],
|
||||
)
|
||||
def test_get_rejects_empty_source_name(
|
||||
source_name: str,
|
||||
) -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
with pytest.raises(
|
||||
InstrumentStoreError,
|
||||
match=r"Имя источника Instrument Store не должно быть пустым",
|
||||
):
|
||||
store.get(source_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_name",
|
||||
[
|
||||
"",
|
||||
" ",
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
],
|
||||
)
|
||||
def test_set_rejects_empty_source_name(
|
||||
source_name: str,
|
||||
) -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
with pytest.raises(
|
||||
InstrumentStoreError,
|
||||
match=r"Имя источника Instrument Store не должно быть пустым",
|
||||
):
|
||||
store.set(
|
||||
source_name,
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source_name",
|
||||
[
|
||||
"",
|
||||
" ",
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
],
|
||||
)
|
||||
def test_targeted_clear_rejects_empty_source_name(
|
||||
source_name: str,
|
||||
) -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
with pytest.raises(
|
||||
InstrumentStoreError,
|
||||
match=r"Имя источника Instrument Store не должно быть пустым",
|
||||
):
|
||||
store.clear(source_name)
|
||||
|
||||
|
||||
def test_set_rejects_list_instead_of_tuple() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
invalid_instruments = cast(
|
||||
tuple[Instrument, ...],
|
||||
[
|
||||
_instrument(),
|
||||
],
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
InstrumentStoreError,
|
||||
match=r"должен быть передан как tuple",
|
||||
):
|
||||
store.set(
|
||||
"dzengi",
|
||||
invalid_instruments,
|
||||
)
|
||||
|
||||
|
||||
def test_set_rejects_non_instrument_item() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
invalid_instruments = cast(
|
||||
tuple[Instrument, ...],
|
||||
(
|
||||
"BTC/USD_LEVERAGE",
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
InstrumentStoreError,
|
||||
match=r"не являющийся Instrument",
|
||||
):
|
||||
store.set(
|
||||
"dzengi",
|
||||
invalid_instruments,
|
||||
)
|
||||
|
||||
|
||||
def test_store_preserves_instrument_order() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
instruments = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
_instrument(
|
||||
symbol="XRP/USD_LEVERAGE",
|
||||
name="XRP/USD",
|
||||
base_asset="XRP",
|
||||
),
|
||||
)
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
instruments,
|
||||
)
|
||||
|
||||
result = store.get("dzengi")
|
||||
|
||||
assert result is not None
|
||||
assert [
|
||||
instrument.symbol
|
||||
for instrument in result
|
||||
] == [
|
||||
"BTC/USD_LEVERAGE",
|
||||
"ETH/USD_LEVERAGE",
|
||||
"XRP/USD_LEVERAGE",
|
||||
]
|
||||
|
||||
|
||||
def test_store_does_not_modify_input_tuple() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
instruments = (
|
||||
_instrument(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
),
|
||||
_instrument(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
name="ETH/USD",
|
||||
base_asset="ETH",
|
||||
),
|
||||
)
|
||||
|
||||
original = instruments
|
||||
|
||||
store.set(
|
||||
"dzengi",
|
||||
instruments,
|
||||
)
|
||||
|
||||
assert instruments is original
|
||||
assert store.get("dzengi") is original
|
||||
|
||||
|
||||
def test_store_instances_are_isolated() -> None:
|
||||
first_store = InMemoryInstrumentStore()
|
||||
second_store = InMemoryInstrumentStore()
|
||||
|
||||
instruments = (
|
||||
_instrument(),
|
||||
)
|
||||
|
||||
first_store.set(
|
||||
"dzengi",
|
||||
instruments,
|
||||
)
|
||||
|
||||
assert first_store.get("dzengi") is instruments
|
||||
assert second_store.get("dzengi") is None
|
||||
|
||||
|
||||
def test_clear_unknown_source_is_idempotent() -> None:
|
||||
store = InMemoryInstrumentStore()
|
||||
|
||||
store.clear("dzengi")
|
||||
store.clear("dzengi")
|
||||
|
||||
assert store.get("dzengi") is None
|
||||
|
||||
|
||||
def test_instrument_store_error_inherits_storage_error() -> None:
|
||||
error = InstrumentStoreError(
|
||||
"Storage failure."
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
error,
|
||||
StorageError,
|
||||
)
|
||||
assert str(error) == "Storage failure."
|
||||
448
app/tests/unit/storage/test_quote_store.py
Normal file
448
app/tests/unit/storage/test_quote_store.py
Normal file
@@ -0,0 +1,448 @@
|
||||
# app/tests/unit/storage/test_quote_store.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.models.quote import Quote
|
||||
from src.storage.exceptions import QuoteStoreError, StorageError
|
||||
from src.storage.quote_store import (
|
||||
InMemoryQuoteStore,
|
||||
QuoteStoreProtocol,
|
||||
)
|
||||
|
||||
|
||||
def _quote(
|
||||
*,
|
||||
symbol: str = "BTC/USD_LEVERAGE",
|
||||
last_price: Decimal = Decimal("64159.45"),
|
||||
source: str = "dzengi",
|
||||
) -> Quote:
|
||||
return Quote(
|
||||
symbol=symbol,
|
||||
last_price=last_price,
|
||||
bid_price=last_price - Decimal("0.05"),
|
||||
ask_price=last_price + Decimal("0.05"),
|
||||
exchange_timestamp=datetime(
|
||||
2026,
|
||||
7,
|
||||
12,
|
||||
18,
|
||||
0,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
received_at=datetime(
|
||||
2026,
|
||||
7,
|
||||
12,
|
||||
18,
|
||||
0,
|
||||
1,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
def test_in_memory_quote_store_matches_protocol() -> None:
|
||||
assert isinstance(
|
||||
InMemoryQuoteStore(),
|
||||
QuoteStoreProtocol,
|
||||
)
|
||||
|
||||
|
||||
def test_get_returns_none_for_unknown_key() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
assert store.get("dzengi", "BTC/USD_LEVERAGE") is None
|
||||
|
||||
|
||||
def test_set_and_get_preserve_quote_identity() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
|
||||
store.set("dzengi", quote)
|
||||
|
||||
assert store.get("dzengi", quote.symbol) is quote
|
||||
|
||||
|
||||
def test_set_replaces_existing_quote() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
first = _quote(last_price=Decimal("100"))
|
||||
second = _quote(last_price=Decimal("101"))
|
||||
|
||||
store.set("dzengi", first)
|
||||
store.set("dzengi", second)
|
||||
|
||||
assert store.get("dzengi", second.symbol) is second
|
||||
|
||||
|
||||
def test_different_sources_are_isolated() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
first = _quote(source="dzengi")
|
||||
second = _quote(source="secondary")
|
||||
|
||||
store.set("dzengi", first)
|
||||
store.set("secondary", second)
|
||||
|
||||
assert store.get("dzengi", first.symbol) is first
|
||||
assert store.get("secondary", second.symbol) is second
|
||||
|
||||
|
||||
def test_source_name_alias_does_not_have_to_match_quote_source() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote(source="dzengi")
|
||||
|
||||
store.set("dzengi-primary", quote)
|
||||
|
||||
assert store.get("dzengi-primary", quote.symbol) is quote
|
||||
|
||||
|
||||
def test_source_name_outer_spaces_are_normalized() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
|
||||
store.set(" dzengi ", quote)
|
||||
|
||||
assert store.get("dzengi", quote.symbol) is quote
|
||||
assert store.get(" dzengi ", quote.symbol) is quote
|
||||
|
||||
|
||||
def test_source_name_case_is_preserved() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
lowercase = _quote(last_price=Decimal("100"))
|
||||
uppercase = _quote(last_price=Decimal("200"))
|
||||
|
||||
store.set("dzengi", lowercase)
|
||||
store.set("DZENGI", uppercase)
|
||||
|
||||
assert store.get("dzengi", lowercase.symbol) is lowercase
|
||||
assert store.get("DZENGI", uppercase.symbol) is uppercase
|
||||
|
||||
|
||||
def test_runtime_keys_are_isolated() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
auto_quote = _quote(last_price=Decimal("100"))
|
||||
debug_quote = _quote(last_price=Decimal("200"))
|
||||
|
||||
store.set("dzengi", auto_quote, runtime_key="auto")
|
||||
store.set("dzengi", debug_quote, runtime_key="debug_auto")
|
||||
|
||||
assert store.get(
|
||||
"dzengi",
|
||||
auto_quote.symbol,
|
||||
runtime_key="auto",
|
||||
) is auto_quote
|
||||
assert store.get(
|
||||
"dzengi",
|
||||
debug_quote.symbol,
|
||||
runtime_key="debug_auto",
|
||||
) is debug_quote
|
||||
|
||||
|
||||
def test_runtime_key_is_trimmed_and_lowercased() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
|
||||
store.set("dzengi", quote, runtime_key=" AUTO ")
|
||||
|
||||
assert store.get(
|
||||
"dzengi",
|
||||
quote.symbol,
|
||||
runtime_key="auto",
|
||||
) is quote
|
||||
assert store.get(
|
||||
"dzengi",
|
||||
quote.symbol,
|
||||
runtime_key=" AUTO ",
|
||||
) is quote
|
||||
|
||||
|
||||
def test_symbols_are_isolated() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
eth = _quote(
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
last_price=Decimal("3500"),
|
||||
)
|
||||
|
||||
store.set("dzengi", btc)
|
||||
store.set("dzengi", eth)
|
||||
|
||||
assert store.get("dzengi", btc.symbol) is btc
|
||||
assert store.get("dzengi", eth.symbol) is eth
|
||||
|
||||
|
||||
def test_symbol_key_is_trimmed_and_uppercased() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote(symbol=" btc/usd_leverage ")
|
||||
|
||||
store.set("dzengi", quote)
|
||||
|
||||
assert store.get("dzengi", "BTC/USD_LEVERAGE") is quote
|
||||
assert store.get("dzengi", " btc/usd_leverage ") is quote
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
|
||||
def test_get_rejects_empty_source_name(source_name: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(
|
||||
QuoteStoreError,
|
||||
match=r"Имя источника Quote Store не должно быть пустым",
|
||||
):
|
||||
store.get(source_name, "BTC/USD_LEVERAGE")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
|
||||
def test_set_rejects_empty_source_name(source_name: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.set(source_name, _quote())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
|
||||
def test_clear_rejects_empty_source_name(source_name: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.clear(source_name=source_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
|
||||
def test_get_rejects_empty_runtime_key(runtime_key: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(
|
||||
QuoteStoreError,
|
||||
match=r"Runtime key Quote Store не должен быть пустым",
|
||||
):
|
||||
store.get(
|
||||
"dzengi",
|
||||
"BTC/USD_LEVERAGE",
|
||||
runtime_key=runtime_key,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
|
||||
def test_set_rejects_empty_runtime_key(runtime_key: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.set(
|
||||
"dzengi",
|
||||
_quote(),
|
||||
runtime_key=runtime_key,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
|
||||
def test_clear_rejects_empty_runtime_key(runtime_key: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.clear(runtime_key=runtime_key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
|
||||
def test_get_rejects_empty_symbol(symbol: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(
|
||||
QuoteStoreError,
|
||||
match=r"Символ Quote Store не должен быть пустым",
|
||||
):
|
||||
store.get("dzengi", symbol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
|
||||
def test_set_rejects_quote_with_empty_symbol(symbol: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.set("dzengi", _quote(symbol=symbol))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
|
||||
def test_clear_rejects_empty_symbol(symbol: str) -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(QuoteStoreError):
|
||||
store.clear(symbol=symbol)
|
||||
|
||||
|
||||
def test_set_rejects_non_quote_object() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
|
||||
with pytest.raises(
|
||||
QuoteStoreError,
|
||||
match=r"принимает только объект Quote",
|
||||
):
|
||||
store.set(
|
||||
"dzengi",
|
||||
cast(Quote, object()),
|
||||
)
|
||||
|
||||
|
||||
def test_clear_without_filters_removes_all_quotes() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
store.set("dzengi", _quote(symbol="BTC/USD_LEVERAGE"))
|
||||
store.set("secondary", _quote(symbol="ETH/USD_LEVERAGE"))
|
||||
|
||||
store.clear()
|
||||
|
||||
assert store.get("dzengi", "BTC/USD_LEVERAGE") is None
|
||||
assert store.get("secondary", "ETH/USD_LEVERAGE") is None
|
||||
|
||||
|
||||
def test_clear_by_source_removes_all_source_quotes() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE")
|
||||
secondary_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
|
||||
store.set("dzengi", dzengi_btc, runtime_key="auto")
|
||||
store.set("dzengi", dzengi_eth, runtime_key="debug_auto")
|
||||
store.set("secondary", secondary_btc, runtime_key="auto")
|
||||
|
||||
store.clear(source_name="dzengi")
|
||||
|
||||
assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None
|
||||
assert store.get("dzengi", dzengi_eth.symbol, runtime_key="debug_auto") is None
|
||||
assert store.get("secondary", secondary_btc.symbol, runtime_key="auto") is secondary_btc
|
||||
|
||||
|
||||
def test_clear_by_runtime_removes_all_runtime_quotes() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
auto_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
auto_eth = _quote(symbol="ETH/USD_LEVERAGE")
|
||||
debug_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
|
||||
store.set("dzengi", auto_btc, runtime_key="auto")
|
||||
store.set("secondary", auto_eth, runtime_key="auto")
|
||||
store.set("dzengi", debug_btc, runtime_key="debug_auto")
|
||||
|
||||
store.clear(runtime_key="AUTO")
|
||||
|
||||
assert store.get("dzengi", auto_btc.symbol, runtime_key="auto") is None
|
||||
assert store.get("secondary", auto_eth.symbol, runtime_key="auto") is None
|
||||
assert store.get("dzengi", debug_btc.symbol, runtime_key="debug_auto") is debug_btc
|
||||
|
||||
|
||||
def test_clear_by_symbol_removes_symbol_from_all_namespaces() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
secondary_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE")
|
||||
|
||||
store.set("dzengi", dzengi_btc, runtime_key="auto")
|
||||
store.set("secondary", secondary_btc, runtime_key="debug_auto")
|
||||
store.set("dzengi", dzengi_eth, runtime_key="auto")
|
||||
|
||||
store.clear(symbol=" btc/usd_leverage ")
|
||||
|
||||
assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None
|
||||
assert store.get("secondary", secondary_btc.symbol, runtime_key="debug_auto") is None
|
||||
assert store.get("dzengi", dzengi_eth.symbol, runtime_key="auto") is dzengi_eth
|
||||
|
||||
|
||||
def test_clear_exact_record_removes_only_that_record() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
auto_quote = _quote()
|
||||
debug_quote = _quote(last_price=Decimal("65000"))
|
||||
|
||||
store.set("dzengi", auto_quote, runtime_key="auto")
|
||||
store.set("dzengi", debug_quote, runtime_key="debug_auto")
|
||||
|
||||
store.clear(
|
||||
source_name="dzengi",
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
runtime_key="auto",
|
||||
)
|
||||
|
||||
assert store.get("dzengi", auto_quote.symbol, runtime_key="auto") is None
|
||||
assert store.get("dzengi", debug_quote.symbol, runtime_key="debug_auto") is debug_quote
|
||||
|
||||
|
||||
def test_clear_combined_source_and_runtime_filters() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
dzengi_auto_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
dzengi_auto_eth = _quote(symbol="ETH/USD_LEVERAGE")
|
||||
dzengi_debug_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
secondary_auto_btc = _quote(symbol="BTC/USD_LEVERAGE")
|
||||
|
||||
store.set("dzengi", dzengi_auto_btc, runtime_key="auto")
|
||||
store.set("dzengi", dzengi_auto_eth, runtime_key="auto")
|
||||
store.set("dzengi", dzengi_debug_btc, runtime_key="debug_auto")
|
||||
store.set("secondary", secondary_auto_btc, runtime_key="auto")
|
||||
|
||||
store.clear(source_name="dzengi", runtime_key="auto")
|
||||
|
||||
assert store.get("dzengi", dzengi_auto_btc.symbol, runtime_key="auto") is None
|
||||
assert store.get("dzengi", dzengi_auto_eth.symbol, runtime_key="auto") is None
|
||||
assert store.get("dzengi", dzengi_debug_btc.symbol, runtime_key="debug_auto") is dzengi_debug_btc
|
||||
assert store.get("secondary", secondary_auto_btc.symbol, runtime_key="auto") is secondary_auto_btc
|
||||
|
||||
|
||||
def test_clear_unknown_record_is_idempotent() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
store.set("dzengi", quote)
|
||||
|
||||
store.clear(
|
||||
source_name="unknown",
|
||||
symbol="ETH/USD_LEVERAGE",
|
||||
runtime_key="missing",
|
||||
)
|
||||
|
||||
assert store.get("dzengi", quote.symbol) is quote
|
||||
|
||||
|
||||
def test_store_instances_are_independent() -> None:
|
||||
first = InMemoryQuoteStore()
|
||||
second = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
|
||||
first.set("dzengi", quote)
|
||||
|
||||
assert first.get("dzengi", quote.symbol) is quote
|
||||
assert second.get("dzengi", quote.symbol) is None
|
||||
|
||||
|
||||
def test_store_preserves_decimal_values() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote(
|
||||
last_price=Decimal("64159.45000001"),
|
||||
)
|
||||
|
||||
store.set("dzengi", quote)
|
||||
result = store.get("dzengi", quote.symbol)
|
||||
|
||||
assert result is not None
|
||||
assert result is quote
|
||||
assert isinstance(result.last_price, Decimal)
|
||||
assert result.last_price == Decimal("64159.45000001")
|
||||
|
||||
|
||||
def test_store_preserves_datetime_values() -> None:
|
||||
store = InMemoryQuoteStore()
|
||||
quote = _quote()
|
||||
|
||||
store.set("dzengi", quote)
|
||||
result = store.get("dzengi", quote.symbol)
|
||||
|
||||
assert result is not None
|
||||
assert result is quote
|
||||
assert result.exchange_timestamp is quote.exchange_timestamp
|
||||
assert result.received_at is quote.received_at
|
||||
|
||||
|
||||
def test_quote_store_error_inherits_storage_error() -> None:
|
||||
error = QuoteStoreError("test")
|
||||
|
||||
assert isinstance(error, StorageError)
|
||||
Reference in New Issue
Block a user