build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View File

@@ -0,0 +1,110 @@
# 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