build 059.12: add runtime event models
This commit is contained in:
114
app/src/market_data/acquisition/runtime/runtime_events.py
Normal file
114
app/src/market_data/acquisition/runtime/runtime_events.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# app/src/market_data/acquisition/runtime/runtime_events.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.market_data.acquisition.runtime.transport_messages import (
|
||||
TransportBinaryMessage,
|
||||
TransportTextMessage,
|
||||
)
|
||||
|
||||
|
||||
TransportMessage = TransportTextMessage | TransportBinaryMessage
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConnectedEvent:
|
||||
"""
|
||||
Событие успешного открытия транспортного соединения.
|
||||
|
||||
Событие фиксирует уже произошедший факт подключения и не содержит
|
||||
логики управления транспортом.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DisconnectedEvent:
|
||||
"""
|
||||
Событие завершения транспортного соединения.
|
||||
|
||||
Событие не определяет причину отключения и не запускает reconnect
|
||||
самостоятельно.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConnectFailedEvent:
|
||||
"""
|
||||
Событие неуспешной попытки открытия транспортного соединения.
|
||||
|
||||
reason содержит безопасное текстовое описание причины ошибки.
|
||||
"""
|
||||
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MessageReceivedEvent:
|
||||
"""
|
||||
Событие получения транспортного сообщения.
|
||||
|
||||
message содержит непрозрачный текстовый или бинарный payload.
|
||||
Runtime Event не интерпретирует содержимое сообщения.
|
||||
"""
|
||||
|
||||
message: TransportMessage
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MessageSentEvent:
|
||||
"""
|
||||
Событие успешной отправки транспортного сообщения.
|
||||
|
||||
message содержит сообщение, переданное транспортному уровню.
|
||||
"""
|
||||
|
||||
message: TransportMessage
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconnectStartedEvent:
|
||||
"""
|
||||
Событие начала попытки восстановления соединения.
|
||||
|
||||
attempt содержит номер текущей попытки reconnect.
|
||||
"""
|
||||
|
||||
attempt: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconnectCompletedEvent:
|
||||
"""
|
||||
Событие успешного завершения восстановления соединения.
|
||||
|
||||
attempt содержит номер попытки, на которой соединение было
|
||||
восстановлено.
|
||||
"""
|
||||
|
||||
attempt: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReconnectFailedEvent:
|
||||
"""
|
||||
Событие неуспешной попытки восстановления соединения.
|
||||
|
||||
attempt содержит номер попытки.
|
||||
reason содержит безопасное текстовое описание причины ошибки.
|
||||
"""
|
||||
|
||||
attempt: int
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HeartbeatTimeoutEvent:
|
||||
"""
|
||||
Событие превышения допустимого интервала heartbeat.
|
||||
|
||||
timeout_seconds содержит настроенный порог ожидания ответа.
|
||||
"""
|
||||
|
||||
timeout_seconds: float
|
||||
@@ -0,0 +1,116 @@
|
||||
# app/tests/unit/market_data/acquisition/runtime/test_runtime_events.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import FrozenInstanceError
|
||||
|
||||
import pytest
|
||||
|
||||
from src.market_data.acquisition.runtime.runtime_events import (
|
||||
ConnectedEvent,
|
||||
ConnectFailedEvent,
|
||||
DisconnectedEvent,
|
||||
HeartbeatTimeoutEvent,
|
||||
MessageReceivedEvent,
|
||||
MessageSentEvent,
|
||||
ReconnectCompletedEvent,
|
||||
ReconnectFailedEvent,
|
||||
ReconnectStartedEvent,
|
||||
)
|
||||
from src.market_data.acquisition.runtime.transport_messages import (
|
||||
TransportBinaryMessage,
|
||||
TransportTextMessage,
|
||||
)
|
||||
|
||||
|
||||
def test_connected_event_is_marker_event() -> None:
|
||||
assert ConnectedEvent() == ConnectedEvent()
|
||||
|
||||
|
||||
def test_disconnected_event_is_marker_event() -> None:
|
||||
assert DisconnectedEvent() == DisconnectedEvent()
|
||||
|
||||
|
||||
def test_connect_failed_event_contains_reason() -> None:
|
||||
event = ConnectFailedEvent(reason="connection refused")
|
||||
|
||||
assert event.reason == "connection refused"
|
||||
|
||||
|
||||
def test_message_received_event_supports_text_message() -> None:
|
||||
message = TransportTextMessage(payload="market-data")
|
||||
event = MessageReceivedEvent(message=message)
|
||||
|
||||
assert event.message is message
|
||||
|
||||
|
||||
def test_message_received_event_supports_binary_message() -> None:
|
||||
message = TransportBinaryMessage(payload=b"\x01\x02")
|
||||
event = MessageReceivedEvent(message=message)
|
||||
|
||||
assert event.message is message
|
||||
|
||||
|
||||
def test_message_sent_event_contains_transport_message() -> None:
|
||||
message = TransportTextMessage(payload="ping")
|
||||
event = MessageSentEvent(message=message)
|
||||
|
||||
assert event.message is message
|
||||
|
||||
|
||||
def test_reconnect_started_event_contains_attempt() -> None:
|
||||
event = ReconnectStartedEvent(attempt=1)
|
||||
|
||||
assert event.attempt == 1
|
||||
|
||||
|
||||
def test_reconnect_completed_event_contains_attempt() -> None:
|
||||
event = ReconnectCompletedEvent(attempt=2)
|
||||
|
||||
assert event.attempt == 2
|
||||
|
||||
|
||||
def test_reconnect_failed_event_contains_attempt_and_reason() -> None:
|
||||
event = ReconnectFailedEvent(
|
||||
attempt=3,
|
||||
reason="timeout",
|
||||
)
|
||||
|
||||
assert event.attempt == 3
|
||||
assert event.reason == "timeout"
|
||||
|
||||
|
||||
def test_heartbeat_timeout_event_contains_timeout_seconds() -> None:
|
||||
event = HeartbeatTimeoutEvent(timeout_seconds=30.0)
|
||||
|
||||
assert event.timeout_seconds == 30.0
|
||||
|
||||
|
||||
def test_connect_failed_event_is_immutable() -> None:
|
||||
event = ConnectFailedEvent(reason="timeout")
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
setattr(event, "reason", "connection refused")
|
||||
|
||||
|
||||
def test_message_received_event_is_immutable() -> None:
|
||||
event = MessageReceivedEvent(
|
||||
message=TransportTextMessage(payload="payload"),
|
||||
)
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
setattr(
|
||||
event,
|
||||
"message",
|
||||
TransportTextMessage(payload="other"),
|
||||
)
|
||||
|
||||
|
||||
def test_reconnect_failed_event_is_immutable() -> None:
|
||||
event = ReconnectFailedEvent(
|
||||
attempt=1,
|
||||
reason="timeout",
|
||||
)
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
setattr(event, "attempt", 2)
|
||||
Reference in New Issue
Block a user