# 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