Загрузка данных


хттп кли
from constants.architecture_constants import HTTPClientConstants as Http_const
from utils.helpers.log_safety import mask_secrets



            if not self.suppress_recv_logging:
                logging.info(f"[HTTP_CLIENT] Выполняю запрос: METHOD: {method} URL: {mask_secrets(url)}")




43стр
        except requests.HTTPError as error:
            logger.error(
                f"[HTTP_CLIENT] [ERROR] При выполнении запроса. METHOD: {method} "
                f"URL: {mask_secrets(url)} ERROR_TEXT:{mask_secrets(error)}"
            )
            raise
        except requests.RequestException as error:
            # traceback печатает текст исключения requests, в который вложен URL, поэтому вместо
            # logger.exception логируем ошибку вручную с маскированием; тип исключения сохраняется
            logger.error(
                f"[HTTP_CLIENT] [ERROR] При выполнении запроса. METHOD: {method} "
                f"URL: {mask_secrets(url)} ERROR_TEXT:{type(error).__name__}: {mask_secrets(error)}"
            )
            raise







            if not self.suppress_recv_logging:
                logging.info(f"[HTTP_CLIENT] Выполняю запрос: METHOD: {method} URL: {mask_secrets(url)}")





        except requests.HTTPError as error:
            logger.error(
                f"[HTTP_CLIENT] [ERROR] При выполнении запроса. METHOD: {method} "
                f"URL: {mask_secrets(url)} ERROR_TEXT:{mask_secrets(error)}"
            )
            raise
        except requests.RequestException as error:
            logger.error(
                f"[HTTP_CLIENT] [ERROR] При выполнении запроса. METHOD: {method} "
                f"URL: {mask_secrets(url)} ERROR_TEXT:{type(error).__name__}: {mask_secrets(error)}"
            )
            raise




























персист вс кли
        return await self._call(self._manager.dispatcher.receive_by_invocation_id, self._scope, invocation_id, timeout)

    def health_snapshot(self) -> dict[str, Any]:
        """Снимок здоровья постоянного соединения: для диагностики падений тестов."""
        return self._manager.health_snapshot()
























сигнал р негот
from constants.architecture_constants import WebSocketClientConstants as WS_Const
from utils.helpers.log_safety import mask_secrets



                   last_error = error
                    logger.warning(
                        "[NEGOTIATE] Попытка %s из %s неуспешна: %s", attempt, max_retries, mask_secrets(error)
                    )
                    if attempt < max_retries:
                        time.sleep(backoff)
        finally:
            self._http_client.suppress_recv_logging = previous_suppression

        raise NegotiationError(f"negotiate не выполнен после {max_retries} попыток: {mask_secrets(last_error)}")



















вс кли 
import time
from collections import Counter




    async def connect(self) -> None:
после текста все тело
        await self._cancel_task(self._ping_task)
        self._ping_task = None
        await self._cancel_task(self._recv_task)
        self._recv_task = None
        if self._ws is not None:
            await self._ws.close()
            self._ws = None
        await self._connect_loop()









        transient_errors = (ConnectionError, OSError, asyncio.TimeoutError, InvalidStatus)
        attempt_reasons: Counter[str] = Counter()




                remaining = deadline - time.monotonic()
                if isinstance(exc, InvalidStatus):
                    stage = "wss-handshake"
                elif isinstance(exc, asyncio.TimeoutError):
                    stage = "handshake-timeout"
                else:
                    stage = type(exc).__name__  # например NegotiationError
                attempt_reasons[stage] += 1
                if remaining <= 0:
                    raise TimeoutError(
                        f"Подключение к hub не удалось: этап={stage}, бюджет={timeout:.0f} с, "
                        f"попыток={attempt}, причины={dict(attempt_reasons)}, "
                        f"последняя ошибка={exc}"
                    ) from exc




                            pause = WS_Const.LB_MISS_RETRY_INTERVAL_S
                logger.warning(
                    "WSS подключение не установлено (попытка %s, этап=%s%s): %s. " "Повтор через %s с, осталось %.0f с",
                    attempt,
                    stage,
                    status_info,
                    exc,
                    pause,
                    remaining,
                )







для 
    def _log_connection_summary(self) -> None:


новое тело 

        logger.info(
            "[WS_HEALTH] connectionId=%s pings_sent=%s pings_failed=%s messages_received=%s " "queue_size=%s",
            self._connection_id,
            self.ws_pings_sent,
            self.ws_pings_failed,
            self.messages_received,
            self.recv_queue.qsize(),
        )







УДАЛИТЬ
async def reconnect(self):
        """
        Переподключение websocket соединения
        """
        try:
            await self._cancel_task(self._ping_task)
            self._ping_task = None
            await self._cancel_task(self._recv_task)
            self._recv_task = None

            if self._ws:
                await self._ws.close()
                self._ws = None
            await self._connect_loop(WS_Const.WS_RECONNECT_TIMEOUT_SECONDS)
            self.reconnect_count += 1
            return self
        except (asyncio.TimeoutError, ConnectionError, ConnectionResetError, OSError) as error:
            raise RuntimeError(f"Ошибка при попытке переподключения {error}")























вс конн мен
from constants.architecture_constants import PersistentConnectionConstants as PersistConst
from constants.architecture_constants import WebSocketClientConstants as WS_Const
from constants.test_constants import BaseTN3Constants as TestConst
from utils.helpers.log_safety import mask_secrets





        self._tasks: List[asyncio.Task] = []
        self._connect_lock: Optional[asyncio.Lock] = None






        self._client = WebSocketClient(self._host, self._access_token, self._x_user_id)
        self._connect_lock = asyncio.Lock()






        return bool(self._client and self._client.is_connected)

    def _get_connect_lock(self) -> Lock | None:
        """
        Лок установки соединения с ленивой инициализацией.

        asyncio.Lock создаётся в том же loop, который будет им пользоваться, 
        первый вызов происходит внутри корутин менеджера.
        """
        if self._connect_lock is None:
            self._connect_lock = asyncio.Lock()
        return self._connect_lock






        while not self._stopped.is_set():
            try:
                async with self._get_connect_lock():
                    await self._client.connect()
                    self.connections_established += 1
                    logger.info(
                        f"[WS_MANAGER] [OK] Постоянное соединение установлено "
                        f"(connectionId={self._client.connection_id})"
                    )
                    await self._subscribe_bundle()
                    await self._restore_scope_subscriptions()
                return







                self.connect_failures += 1
                logger.warning(
                    f"[WS_MANAGER] Не удалось установить соединение: {mask_secrets(error)}. "
                    f"Повтор через {backoff:.0f} с"
                )











                self.rest_pings_failed += 1
                logger.warning(f"[WS_MANAGER] REST Ping не выполнен: {mask_secrets(error)}")









удалить
            "clientReconnects": client.reconnect_count if client else 0,






удалить
    async def reconnect_now(self) -> None:
        """Принудительное переподключение с восстановлением bundle и подписок тестов."""
        self._ensure_client()
        await self._client.reconnect()
        await self._subscribe_bundle()
        await self._restore_scope_subscriptions()























вс месс диспатчер
    def register_subscription(self, scope: SubscriptionScope, target: str, args: Any) -> None:
        """
        Запоминает подписку скоупа для повторной отправки после реконнекта.
        """
        key = (target, repr(args))
        known = {(subscription[0], repr(subscription[1])) for subscription in scope.subscriptions}
        if key not in known:
            scope.subscriptions.append((target, args))





























арх конст
удалить
    WS_RECONNECT_TIMEOUT_SECONDS: float = 20.0





    SIGNAL_STATE_POLL_ATTEMPTS = 12  # Число опросов состояния датчика
    SUBSCRIBE_REPLY_TIMEOUT_SECONDS = 60.0






























вс тест утился
    fail(
        f"При {purpose} за {total_wait_seconds} с не получен StreamItem с fileChunk "
        f"({request_name}, invocation_id={invocation_id}). Смотреть вложения received ws message"
    )


def describe_connection_health(ws_client: Any) -> str:
    """
    Однострочное состояние ws-соединения для текста падения теста.
    """
    try:
        if hasattr(ws_client, "health_snapshot"):
            snapshot = ws_client.health_snapshot()
            return (
                f"Состояние ws-соединения: isConnected={snapshot.get('isConnected')}, "
                f"connectionId={snapshot.get('connectionId')}, "
                f"установлено соединений={snapshot.get('connectionsEstablished')}, "
                f"ошибок подключения={snapshot.get('connectFailures')}, "
                f"ws-пинги={snapshot.get('wsPingsSent')}/{snapshot.get('wsPingsFailed')}, "
                f"rest-пинги={snapshot.get('restPingsSent')}/{snapshot.get('restPingsFailed')}, "
                f"принято сообщений={snapshot.get('messagesReceived')}, "
                f"доставлено в тесты={snapshot.get('messagesRouted')}"
            )
        # Разовый клиент (flow LDS Configurator): его счётчики копятся на одном соединении
        return (
            f"Состояние ws-соединения: isConnected={ws_client.is_connected}, "
            f"connectionId={ws_client.connection_id}, "
            f"ws-пинги={ws_client.ws_pings_sent}/{ws_client.ws_pings_failed}, "
            f"принято сообщений={ws_client.messages_received}"
        )
    except Exception as error:  # диагностика не должна маскировать исходное падение
        return f"Состояние ws-соединения: недоступно ({type(error).__name__}: {error})"










функц целиком
async def _receive_subscribed_messages(
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any,
    *,
    attempts: int,
    per_attempt_timeout: float,
) -> AsyncIterator[list]:
    """
    Генератор сообщений подписки: подписывается один раз и последовательно отдаёт
    сообщения нужного типа до исчерпания запаса времени.
    """
    await connect(ws_client, ws_invoke_type, ws_invoke_params)
    for attempt in range(1, attempts + 1):
        with allure.step(
            f"Получение сообщения с контентом типа: {ws_message_type} - попытка {attempt} из {attempts} "
            f"(до {per_attempt_timeout:.0f} с)"
        ):
            try:
                message = await ws_client.receive_by_type(ws_message_type, timeout=per_attempt_timeout)
            except asyncio.TimeoutError:
                continue
        yield message





    Подписывается и ищет allLeaksInfo сообщение для конкретного ТУ.
    Читает сообщения нужного типа с ретраями, фильтруя по tuId.




        if parsed_payload.replyContent.tuId == tu_id:
            return parsed_payload

    fail(
        f"Не удалось получить сообщение {ws_message_type} для ТУ {tu_id} за {attempts} попыток "
        f"(лимит ~{attempts * TestConst.POLL_BY_TIMEOUT_SECONDS:.0f} с). "
        f"{describe_connection_health(ws_client)}"
    )









вся функция
async def connect_and_poll_subscribed_msg(
    ws_client: WebSocketClient,
    ws_message_type: str,
    ws_invoke_type: str,
    ws_invoke_params: Any = None,
) -> list:
    """
    Подписывается и ждёт нужное сообщение одним чтением до SUBSCRIBE_REPLY_TIMEOUT_SECONDS.
    """
    ws_client.suppress_recv_logging = True
    try:
        async for payload in _receive_subscribed_messages(
            ws_client,
            ws_message_type,
            ws_invoke_type,
            ws_invoke_params,
            attempts=1,
            per_attempt_timeout=TestConst.SUBSCRIBE_REPLY_TIMEOUT_SECONDS,
        ):
            return payload
        fail(
            f"Бэкенд не прислал {ws_message_type} за {TestConst.SUBSCRIBE_REPLY_TIMEOUT_SECONDS:.0f} с "
            f"после подписки {ws_invoke_type} {ws_invoke_params}. {describe_connection_health(ws_client)}"
        )
    finally:
        ws_client.suppress_recv_logging = False









    fail(
        f"Сигналы id={sensor_ids} не перешли в состояние {flag_field}={expected_value} "
        f"за {attempts} попыток (лимит ~{attempts * per_attempt_timeout:.0f} с). "
        f"{describe_connection_health(ws_client)}"
    )






) -> tuple[ParsedPayloadType, InputSignalType]:
    """
    Подписывается и читает до max_messages сообщений из потока, пока не найдёт сигнал
    с нужным id.
    """







    fail(
        f"Информация по датчику {sensor_description} (id={sensor_id}) не пришла в ответе {ws_message_type} "
        f"после {max_messages} попыток чтения из потока подписки "
        f"(лимит ~{max_messages * timeout:.0f} с). Полученные id: {received_ids_text}. "
        f"{describe_connection_health(ws_client)}"
    )


























utils/helpers/log_safety.py
"""
Маскирование секретов в текстах логов и сообщений об ошибках.
"""

from __future__ import annotations

import re
from typing import Any

# token=<значение> в query-строке: значение идёт до разделителя параметра, пробела или кавычки
_TOKEN_QUERY_PATTERN = re.compile(r"(?i)([?&]token=)[^&\s'\"]+")
# standalone JWT (header.payload.signature), например внутри текста исключения
_JWT_PATTERN = re.compile(r"eyJ[\w-]+\.[\w-]+\.[\w-]+")

MASK: str = "***"


def mask_secrets(text: Any) -> str:
    """
    Возвращает текст с замаскированными секретами.
    """
    if text is None:
        return ""
    masked = _TOKEN_QUERY_PATTERN.sub(rf"\g<1>{MASK}", str(text))
    return _JWT_PATTERN.sub(MASK, masked)