# app/src/storage/instrument_store.py from __future__ import annotations from typing import Protocol, runtime_checkable from src.market_data.acquisition.models.instrument import Instrument from src.storage.exceptions import InstrumentStoreError # Контракт runtime-хранилища канонического справочника инструментов. @runtime_checkable class InstrumentStoreProtocol(Protocol): def get( self, source_name: str, ) -> tuple[Instrument, ...] | None: """ Вернуть сохранённый набор инструментов для источника. None означает cache miss: данные для источника ещё не сохранялись. Пустой tuple означает успешное сохранение пустого справочника. """ def set( self, source_name: str, instruments: tuple[Instrument, ...], ) -> None: """Сохранить полный immutable-набор инструментов источника.""" def clear( self, source_name: str | None = None, ) -> None: """ Очистить данные одного источника или всё хранилище. source_name=None очищает все сохранённые источники. """ # In-memory реализация runtime-хранилища справочника инструментов. class InMemoryInstrumentStore: def __init__(self) -> None: self._items: dict[str, tuple[Instrument, ...]] = {} def get( self, source_name: str, ) -> tuple[Instrument, ...] | None: normalized_source_name = self._normalize_source_name( source_name ) return self._items.get(normalized_source_name) def set( self, source_name: str, instruments: tuple[Instrument, ...], ) -> None: normalized_source_name = self._normalize_source_name( source_name ) if not isinstance(instruments, tuple): raise InstrumentStoreError( "Справочник инструментов должен быть передан как tuple." ) if not all( isinstance(instrument, Instrument) for instrument in instruments ): raise InstrumentStoreError( "Справочник содержит объект, не являющийся Instrument." ) self._items[normalized_source_name] = instruments def clear( self, source_name: str | None = None, ) -> None: if source_name is None: self._items.clear() return normalized_source_name = self._normalize_source_name( source_name ) self._items.pop( normalized_source_name, None, ) def _normalize_source_name( self, source_name: str, ) -> str: normalized_source_name = str(source_name or "").strip() if not normalized_source_name: raise InstrumentStoreError( "Имя источника Instrument Store не должно быть пустым." ) return normalized_source_name