227 lines
5.9 KiB
Python
227 lines
5.9 KiB
Python
# app/src/storage/quote_store.py
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Protocol, runtime_checkable
|
|
|
|
from src.market_data.acquisition.models.quote import Quote
|
|
from src.storage.exceptions import QuoteStoreError
|
|
|
|
|
|
# Контракт runtime-хранилища канонических котировок.
|
|
@runtime_checkable
|
|
class QuoteStoreProtocol(Protocol):
|
|
def get(
|
|
self,
|
|
source_name: str,
|
|
symbol: str,
|
|
*,
|
|
runtime_key: str = "default",
|
|
) -> Quote | None:
|
|
"""Вернуть котировку или None, если запись отсутствует."""
|
|
|
|
def set(
|
|
self,
|
|
source_name: str,
|
|
quote: Quote,
|
|
*,
|
|
runtime_key: str = "default",
|
|
) -> None:
|
|
"""Сохранить каноническую котировку без копирования модели."""
|
|
|
|
def clear(
|
|
self,
|
|
source_name: str | None = None,
|
|
symbol: str | None = None,
|
|
*,
|
|
runtime_key: str | None = None,
|
|
) -> None:
|
|
"""Удалить записи, соответствующие переданным фильтрам."""
|
|
|
|
|
|
# In-memory реализация runtime-хранилища канонических котировок.
|
|
class InMemoryQuoteStore:
|
|
def __init__(self) -> None:
|
|
self._items: dict[tuple[str, str, str], Quote] = {}
|
|
|
|
def get(
|
|
self,
|
|
source_name: str,
|
|
symbol: str,
|
|
*,
|
|
runtime_key: str = "default",
|
|
) -> Quote | None:
|
|
return self._items.get(
|
|
self._key(
|
|
source_name=source_name,
|
|
symbol=symbol,
|
|
runtime_key=runtime_key,
|
|
)
|
|
)
|
|
|
|
def set(
|
|
self,
|
|
source_name: str,
|
|
quote: Quote,
|
|
*,
|
|
runtime_key: str = "default",
|
|
) -> None:
|
|
normalized_source_name = self._normalize_source_name(
|
|
source_name
|
|
)
|
|
normalized_runtime_key = self._normalize_runtime_key(
|
|
runtime_key
|
|
)
|
|
|
|
if not isinstance(quote, Quote):
|
|
raise QuoteStoreError(
|
|
"Quote Store принимает только объект Quote."
|
|
)
|
|
|
|
normalized_symbol = self._normalize_symbol(
|
|
quote.symbol
|
|
)
|
|
|
|
self._items[
|
|
(
|
|
normalized_source_name,
|
|
normalized_runtime_key,
|
|
normalized_symbol,
|
|
)
|
|
] = quote
|
|
|
|
def clear(
|
|
self,
|
|
source_name: str | None = None,
|
|
symbol: str | None = None,
|
|
*,
|
|
runtime_key: str | None = None,
|
|
) -> None:
|
|
if (
|
|
source_name is None
|
|
and symbol is None
|
|
and runtime_key is None
|
|
):
|
|
self._items.clear()
|
|
return
|
|
|
|
normalized_source_name = (
|
|
self._normalize_source_name(source_name)
|
|
if source_name is not None
|
|
else None
|
|
)
|
|
normalized_symbol = (
|
|
self._normalize_symbol(symbol)
|
|
if symbol is not None
|
|
else None
|
|
)
|
|
normalized_runtime_key = (
|
|
self._normalize_runtime_key(runtime_key)
|
|
if runtime_key is not None
|
|
else None
|
|
)
|
|
|
|
keys_to_delete = [
|
|
key
|
|
for key in self._items
|
|
if self._matches_filters(
|
|
key,
|
|
source_name=normalized_source_name,
|
|
symbol=normalized_symbol,
|
|
runtime_key=normalized_runtime_key,
|
|
)
|
|
]
|
|
|
|
for key in keys_to_delete:
|
|
self._items.pop(key, None)
|
|
|
|
def _key(
|
|
self,
|
|
*,
|
|
source_name: str,
|
|
symbol: str,
|
|
runtime_key: str,
|
|
) -> tuple[str, str, str]:
|
|
return (
|
|
self._normalize_source_name(source_name),
|
|
self._normalize_runtime_key(runtime_key),
|
|
self._normalize_symbol(symbol),
|
|
)
|
|
|
|
def _matches_filters(
|
|
self,
|
|
key: tuple[str, str, str],
|
|
*,
|
|
source_name: str | None,
|
|
symbol: str | None,
|
|
runtime_key: str | None,
|
|
) -> bool:
|
|
key_source_name, key_runtime_key, key_symbol = key
|
|
|
|
if (
|
|
source_name is not None
|
|
and key_source_name != source_name
|
|
):
|
|
return False
|
|
|
|
if (
|
|
runtime_key is not None
|
|
and key_runtime_key != runtime_key
|
|
):
|
|
return False
|
|
|
|
if symbol is not None and key_symbol != symbol:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _normalize_source_name(
|
|
self,
|
|
source_name: str,
|
|
) -> str:
|
|
normalized_source_name = str(source_name or "").strip()
|
|
|
|
if not normalized_source_name:
|
|
raise QuoteStoreError(
|
|
"Имя источника Quote Store не должно быть пустым."
|
|
)
|
|
|
|
return normalized_source_name
|
|
|
|
def _normalize_runtime_key(
|
|
self,
|
|
runtime_key: str,
|
|
) -> str:
|
|
normalized_runtime_key = str(runtime_key or "").strip().lower()
|
|
|
|
if not normalized_runtime_key:
|
|
raise QuoteStoreError(
|
|
"Runtime key Quote Store не должен быть пустым."
|
|
)
|
|
|
|
return normalized_runtime_key
|
|
|
|
def _normalize_symbol(
|
|
self,
|
|
symbol: str,
|
|
) -> str:
|
|
normalized_symbol = str(symbol or "").strip().upper()
|
|
|
|
if not normalized_symbol:
|
|
raise QuoteStoreError(
|
|
"Символ Quote Store не должен быть пустым."
|
|
)
|
|
|
|
return normalized_symbol
|
|
|
|
|
|
|
|
QUOTE_RUNTIME_SOURCE_NAME = "legacy-market-price-cache"
|
|
_SHARED_QUOTE_STORE: QuoteStoreProtocol = InMemoryQuoteStore()
|
|
|
|
|
|
def get_quote_store() -> QuoteStoreProtocol:
|
|
"""Вернуть общий runtime-экземпляр канонического Quote Store."""
|
|
return _SHARED_QUOTE_STORE
|
|
|