build 039: complete Quotes Feed migration foundation
This commit is contained in:
215
app/src/storage/quote_store.py
Normal file
215
app/src/storage/quote_store.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user