unicex 0.13.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. unicex/__init__.py +200 -0
  2. unicex/_abc/__init__.py +11 -0
  3. unicex/_abc/exchange_info.py +216 -0
  4. unicex/_abc/uni_client.py +329 -0
  5. unicex/_abc/uni_websocket_manager.py +294 -0
  6. unicex/_base/__init__.py +9 -0
  7. unicex/_base/client.py +214 -0
  8. unicex/_base/websocket.py +261 -0
  9. unicex/binance/__init__.py +27 -0
  10. unicex/binance/adapter.py +202 -0
  11. unicex/binance/client.py +1577 -0
  12. unicex/binance/exchange_info.py +62 -0
  13. unicex/binance/uni_client.py +188 -0
  14. unicex/binance/uni_websocket_manager.py +166 -0
  15. unicex/binance/user_websocket.py +186 -0
  16. unicex/binance/websocket_manager.py +912 -0
  17. unicex/bitget/__init__.py +27 -0
  18. unicex/bitget/adapter.py +188 -0
  19. unicex/bitget/client.py +2514 -0
  20. unicex/bitget/exchange_info.py +48 -0
  21. unicex/bitget/uni_client.py +198 -0
  22. unicex/bitget/uni_websocket_manager.py +275 -0
  23. unicex/bitget/user_websocket.py +7 -0
  24. unicex/bitget/websocket_manager.py +232 -0
  25. unicex/bybit/__init__.py +27 -0
  26. unicex/bybit/adapter.py +208 -0
  27. unicex/bybit/client.py +1876 -0
  28. unicex/bybit/exchange_info.py +53 -0
  29. unicex/bybit/uni_client.py +200 -0
  30. unicex/bybit/uni_websocket_manager.py +291 -0
  31. unicex/bybit/user_websocket.py +7 -0
  32. unicex/bybit/websocket_manager.py +339 -0
  33. unicex/enums.py +273 -0
  34. unicex/exceptions.py +64 -0
  35. unicex/extra.py +335 -0
  36. unicex/gate/__init__.py +27 -0
  37. unicex/gate/adapter.py +178 -0
  38. unicex/gate/client.py +1667 -0
  39. unicex/gate/exchange_info.py +55 -0
  40. unicex/gate/uni_client.py +214 -0
  41. unicex/gate/uni_websocket_manager.py +269 -0
  42. unicex/gate/user_websocket.py +7 -0
  43. unicex/gate/websocket_manager.py +513 -0
  44. unicex/hyperliquid/__init__.py +27 -0
  45. unicex/hyperliquid/adapter.py +261 -0
  46. unicex/hyperliquid/client.py +2315 -0
  47. unicex/hyperliquid/exchange_info.py +119 -0
  48. unicex/hyperliquid/uni_client.py +325 -0
  49. unicex/hyperliquid/uni_websocket_manager.py +269 -0
  50. unicex/hyperliquid/user_websocket.py +7 -0
  51. unicex/hyperliquid/websocket_manager.py +393 -0
  52. unicex/mapper.py +111 -0
  53. unicex/mexc/__init__.py +27 -0
  54. unicex/mexc/_spot_ws_proto/PrivateAccountV3Api_pb2.py +38 -0
  55. unicex/mexc/_spot_ws_proto/PrivateDealsV3Api_pb2.py +38 -0
  56. unicex/mexc/_spot_ws_proto/PrivateOrdersV3Api_pb2.py +38 -0
  57. unicex/mexc/_spot_ws_proto/PublicAggreBookTickerV3Api_pb2.py +38 -0
  58. unicex/mexc/_spot_ws_proto/PublicAggreDealsV3Api_pb2.py +40 -0
  59. unicex/mexc/_spot_ws_proto/PublicAggreDepthsV3Api_pb2.py +40 -0
  60. unicex/mexc/_spot_ws_proto/PublicBookTickerBatchV3Api_pb2.py +38 -0
  61. unicex/mexc/_spot_ws_proto/PublicBookTickerV3Api_pb2.py +38 -0
  62. unicex/mexc/_spot_ws_proto/PublicDealsV3Api_pb2.py +40 -0
  63. unicex/mexc/_spot_ws_proto/PublicFuture_pb2.py +103 -0
  64. unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsBatchV3Api_pb2.py +38 -0
  65. unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsV3Api_pb2.py +40 -0
  66. unicex/mexc/_spot_ws_proto/PublicLimitDepthsV3Api_pb2.py +40 -0
  67. unicex/mexc/_spot_ws_proto/PublicMiniTickerV3Api_pb2.py +38 -0
  68. unicex/mexc/_spot_ws_proto/PublicMiniTickersV3Api_pb2.py +38 -0
  69. unicex/mexc/_spot_ws_proto/PublicSpotKlineV3Api_pb2.py +38 -0
  70. unicex/mexc/_spot_ws_proto/PushDataV3ApiWrapper_pb2.py +38 -0
  71. unicex/mexc/_spot_ws_proto/__init__.py +335 -0
  72. unicex/mexc/adapter.py +239 -0
  73. unicex/mexc/client.py +846 -0
  74. unicex/mexc/exchange_info.py +47 -0
  75. unicex/mexc/uni_client.py +211 -0
  76. unicex/mexc/uni_websocket_manager.py +269 -0
  77. unicex/mexc/user_websocket.py +7 -0
  78. unicex/mexc/websocket_manager.py +456 -0
  79. unicex/okx/__init__.py +27 -0
  80. unicex/okx/adapter.py +150 -0
  81. unicex/okx/client.py +2864 -0
  82. unicex/okx/exchange_info.py +47 -0
  83. unicex/okx/uni_client.py +202 -0
  84. unicex/okx/uni_websocket_manager.py +269 -0
  85. unicex/okx/user_websocket.py +7 -0
  86. unicex/okx/websocket_manager.py +743 -0
  87. unicex/types.py +164 -0
  88. unicex/utils.py +218 -0
  89. unicex-0.13.17.dist-info/METADATA +243 -0
  90. unicex-0.13.17.dist-info/RECORD +93 -0
  91. unicex-0.13.17.dist-info/WHEEL +5 -0
  92. unicex-0.13.17.dist-info/licenses/LICENSE +28 -0
  93. unicex-0.13.17.dist-info/top_level.txt +1 -0
unicex/_base/client.py ADDED
@@ -0,0 +1,214 @@
1
+ __all__ = ["BaseClient"]
2
+
3
+ import asyncio
4
+ import json
5
+ from itertools import cycle
6
+ from typing import Any, Self
7
+
8
+ import aiohttp
9
+ from loguru import logger as _logger
10
+
11
+ from unicex.exceptions import ResponseError
12
+ from unicex.types import LoggerLike, RequestMethod
13
+
14
+
15
+ class BaseClient:
16
+ """Базовый асинхронный класс для работы с API."""
17
+
18
+ def __init__(
19
+ self,
20
+ session: aiohttp.ClientSession,
21
+ api_key: str | None = None,
22
+ api_secret: str | None = None,
23
+ api_passphrase: str | None = None,
24
+ logger: LoggerLike | None = None,
25
+ max_retries: int = 3,
26
+ retry_delay: int | float = 0.1,
27
+ proxies: list[str] | None = None,
28
+ timeout: int = 10,
29
+ ) -> None:
30
+ """Инициализация клиента.
31
+
32
+ Параметры:
33
+ session (`aiohttp.ClientSession`): Сессия для выполнения HTTP‑запросов.
34
+ api_key (`str | None`): Ключ API для аутентификации.
35
+ api_secret (`str | None`): Секретный ключ API для аутентификации.
36
+ api_passphrase (`str | None`): Пароль API для аутентификации (Bitget, OKX).
37
+ logger (`LoggerLike | None`): Логгер для вывода информации.
38
+ max_retries (`int`): Максимальное количество повторных попыток запроса.
39
+ retry_delay (`int | float`): Задержка между повторными попытками, сек.
40
+ proxies (`list[str] | None`): Список HTTP(S)‑прокси для циклического использования.
41
+ timeout (`int`): Максимальное время ожидания ответа от сервера, сек.
42
+ """
43
+ self._api_key = api_key
44
+ self._api_secret = api_secret
45
+ self._api_passphrase = api_passphrase
46
+ self._session = session
47
+ self._logger = logger or _logger
48
+ self._max_retries = max(max_retries, 1)
49
+ self._retry_delay = max(retry_delay, 0)
50
+ self._proxies_cycle = cycle(proxies) if proxies else None
51
+ self._timeout = timeout
52
+
53
+ @classmethod
54
+ async def create(
55
+ cls,
56
+ api_key: str | None = None,
57
+ api_secret: str | None = None,
58
+ api_passphrase: str | None = None,
59
+ session: aiohttp.ClientSession | None = None,
60
+ logger: LoggerLike | None = None,
61
+ max_retries: int = 3,
62
+ retry_delay: int | float = 0.1,
63
+ proxies: list[str] | None = None,
64
+ timeout: int = 10,
65
+ ) -> Self:
66
+ """Создаёт инстанцию клиента.
67
+
68
+ Параметры:
69
+ api_key (`str | None`): Ключ API для аутентификации.
70
+ api_secret (`str | None`): Секретный ключ API для аутентификации.
71
+ api_passphrase (`str | None`): Пароль API для аутентификации (Bitget, OKX).
72
+ session (`aiohttp.ClientSession | None`): Сессия для HTTP‑запросов (если не передана, будет создана).
73
+ logger (`LoggerLike | None`): Логгер для вывода информации.
74
+ max_retries (`int`): Максимум повторов при ошибках запроса.
75
+ retry_delay (`int | float`): Задержка между повторами, сек.
76
+ proxies (`list[str] | None`): Список HTTP(S)‑прокси.
77
+ timeout (`int`): Таймаут ответа сервера, сек.
78
+
79
+ Возвращает:
80
+ `Self`: Созданный экземпляр клиента.
81
+ """
82
+ return cls(
83
+ session=session or aiohttp.ClientSession(),
84
+ api_key=api_key,
85
+ api_secret=api_secret,
86
+ api_passphrase=api_passphrase,
87
+ logger=logger,
88
+ max_retries=max_retries,
89
+ retry_delay=retry_delay,
90
+ proxies=proxies,
91
+ timeout=timeout,
92
+ )
93
+
94
+ async def close_connection(self) -> None:
95
+ """Закрывает сессию."""
96
+ await self._session.close()
97
+
98
+ def is_authorized(self) -> bool:
99
+ """Проверяет наличие API‑ключей у клиента.
100
+
101
+ Возвращает:
102
+ `bool`: Признак наличия ключей.
103
+ """
104
+ return self._api_key is not None and self._api_secret is not None
105
+
106
+ async def __aenter__(self) -> Self:
107
+ """Вход в асинхронный контекст."""
108
+ return self
109
+
110
+ async def __aexit__(self, *_) -> None:
111
+ """Выход из асинхронного контекста."""
112
+ await self.close_connection()
113
+
114
+ async def _make_request(
115
+ self,
116
+ method: RequestMethod,
117
+ url: str,
118
+ params: dict[str, Any] | None = None,
119
+ data: dict[str, Any] | None = None,
120
+ headers: dict[str, Any] | None = None,
121
+ ) -> Any:
122
+ """Выполняет HTTP‑запрос к API биржи.
123
+
124
+ Параметры:
125
+ method (`RequestMethod`): HTTP‑метод запроса.
126
+ url (`str`): Полный URL API.
127
+ params (`dict[str, Any] | None`): Параметры запроса (query string).
128
+ data (`dict[str, Any] | None`): Тело запроса для POST/PUT.
129
+ headers (`dict[str, Any] | None`): Заголовки запроса.
130
+
131
+ Возвращает:
132
+ `dict | list`: Ответ API в формате JSON.
133
+ """
134
+ self._logger.debug(
135
+ f"Request: {method} {url} | Params: {params} | Data: {data} | Headers: {headers}"
136
+ )
137
+
138
+ errors = []
139
+ for attempt in range(1, self._max_retries + 1):
140
+ try:
141
+ async with self._session.request(
142
+ method=method,
143
+ url=url,
144
+ params=params,
145
+ json=data if method in {"POST", "PUT"} else None, # Передача тела запроса
146
+ headers=headers,
147
+ proxy=next(self._proxies_cycle) if self._proxies_cycle else None,
148
+ timeout=aiohttp.ClientTimeout(total=self._timeout) if self._timeout else None,
149
+ ) as response:
150
+ return await self._handle_response(response=response)
151
+
152
+ except (aiohttp.ServerTimeoutError, aiohttp.ConnectionTimeoutError) as e:
153
+ errors.append(e)
154
+ self._logger.debug(
155
+ f"Attempt {attempt}/{self._max_retries} failed: {type(e)} -> {e}"
156
+ )
157
+ if attempt < self._max_retries:
158
+ await asyncio.sleep(self._retry_delay)
159
+
160
+ raise ConnectionError(
161
+ f"Connection error after {self._max_retries} request on {method} {url}. Errors: {errors}"
162
+ ) from errors[-1]
163
+
164
+ async def _handle_response(self, response: aiohttp.ClientResponse) -> Any:
165
+ """Обрабатывает HTTP‑ответ.
166
+
167
+ Параметры:
168
+ response (`aiohttp.ClientResponse`): Ответ HTTP‑запроса.
169
+
170
+ Возвращает:
171
+ `dict | list`: Ответ API в формате JSON.
172
+ """
173
+ response_text = await response.text()
174
+ status_code = response.status
175
+
176
+ # Парсинг JSON
177
+ try:
178
+ response_json = json.loads(response_text)
179
+ except json.JSONDecodeError as e:
180
+ raise ResponseError(
181
+ f"JSONDecodeError: {e}. Response: {response_text}. Status code: {response.status}",
182
+ status_code=status_code,
183
+ response_text=response_text,
184
+ ) from None
185
+
186
+ # Проверка HTTP-статуса
187
+ try:
188
+ response.raise_for_status()
189
+ except Exception as e:
190
+ error_code = next(
191
+ (
192
+ response_json[k]
193
+ for k in ("code", "err_code", "errCode", "status")
194
+ if k in response_json
195
+ ),
196
+ "",
197
+ )
198
+ raise ResponseError(
199
+ f"HTTP error: {e}. Response: {response_json}. Status code: {response.status}",
200
+ status_code=status_code,
201
+ code=error_code,
202
+ response_text=response_text,
203
+ response_json=response_json,
204
+ ) from None
205
+
206
+ # Логирование ответа
207
+ try:
208
+ self._logger.debug(
209
+ f"Response: {response_text[:300]}{'...' if len(response_text) > 300 else ''}"
210
+ )
211
+ except Exception as e:
212
+ self._logger.error(f"Error while logging response: {e}")
213
+
214
+ return response_json
@@ -0,0 +1,261 @@
1
+ __all__ = ["Websocket"]
2
+
3
+ import asyncio
4
+ import time
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any, Protocol
7
+
8
+ import orjson
9
+ import websockets
10
+ from loguru import logger as _logger
11
+ from websockets.asyncio.client import ClientConnection
12
+
13
+ from unicex.exceptions import QueueOverflowError
14
+ from unicex.types import LoggerLike
15
+
16
+
17
+ class Websocket:
18
+ """Базовый класс асинхронного вебсокета."""
19
+
20
+ MAX_QUEUE_SIZE: int = 500
21
+ """Максимальная длина очереди."""
22
+
23
+ class _DecoderProtocol(Protocol):
24
+ """Протокол декодирования сообщений."""
25
+
26
+ def decode(self, message: Any) -> dict: ...
27
+
28
+ class _JsonDecoder:
29
+ """Протокол декодирования сообщений в формате JSON."""
30
+
31
+ def decode(self, message: Any) -> dict:
32
+ return orjson.loads(message)
33
+
34
+ def __init__(
35
+ self,
36
+ callback: Callable[[Any], Awaitable[None]],
37
+ url: str,
38
+ subscription_messages: list[dict] | list[str] | None = None,
39
+ ping_interval: int | float = 10,
40
+ ping_message: str | Callable | None = None,
41
+ pong_message: str | Callable | None = None,
42
+ no_message_reconnect_timeout: int | float | None = 60,
43
+ reconnect_timeout: int | float | None = 5,
44
+ worker_count: int = 1,
45
+ logger: LoggerLike | None = None,
46
+ decoder: type[_DecoderProtocol] = _JsonDecoder,
47
+ **kwargs: Any, # Не дадим сломаться, если юзер передал ненужные аргументы
48
+ ) -> None:
49
+ """Инициализация вебсокета.
50
+
51
+ Параметры:
52
+ callback (`Callable[[Any], Awaitable[None]]`): Обработчик входящих сообщений.
53
+ url (`str`): URL вебсокета.
54
+ subscription_messages (`list[dict] | list[str] | None`): Сообщения для подписки после подключения.
55
+ ping_interval (`int | float`): Интервал отправки ping, сек.
56
+ ping_message (`str | Callable | None`): Сообщение для ping, или функция генерации ping (если не указано — используется ping‑frame).
57
+ pong_message (`str | Callable | None`): Сообщение для pong, или функция генерации pong (если не указано — используется pong‑frame).
58
+ no_message_reconnect_timeout (`int | float | None`): Таймаут ожидания без сообщений до рестарта, сек.
59
+ reconnect_timeout (`int | float | None`): Пауза перед переподключением, сек.
60
+ worker_count (`int`): Количество рабочих задач для обработки сообщений.
61
+ logger (`LoggerLike | None`): Логгер для записи логов.
62
+ decoder (`IDecoder | None`): Декодер для обработки входящих сообщений.
63
+ """
64
+ self._callback = callback
65
+ self._url = url
66
+ self._subscription_messages = subscription_messages or []
67
+ self._ping_interval = ping_interval
68
+ self._ping_message = ping_message
69
+ self._pong_message = pong_message
70
+ self._no_message_reconnect_timeout = no_message_reconnect_timeout
71
+ self._reconnect_timeout = reconnect_timeout or 0
72
+ self._last_message_time = time.monotonic()
73
+ self._worker_count = worker_count
74
+ self._logger = logger or _logger
75
+ self._decoder = decoder()
76
+ self._tasks: list[asyncio.Task] = []
77
+ self._queue = asyncio.Queue()
78
+ self._running = False
79
+
80
+ async def start(self) -> None:
81
+ """Запускает вебсокет и рабочие задачи."""
82
+ # Проверяем что вебсокет еще не запущен
83
+ if self._running:
84
+ raise RuntimeError("Websocket is already running")
85
+ self._running = True
86
+
87
+ # Запускаем вебсокет
88
+ try:
89
+ await self._connect()
90
+ except Exception as e:
91
+ self._logger.error(f"Failed to connect to websocket: {e}")
92
+ self._running = False
93
+ raise
94
+
95
+ async def stop(self) -> None:
96
+ """Останавливает вебсокет и рабочие задачи."""
97
+ self._running = False
98
+ await self._after_disconnect()
99
+
100
+ async def restart(self) -> None:
101
+ """Перезапускает вебсокет."""
102
+ await self.stop()
103
+ await asyncio.sleep(self._reconnect_timeout)
104
+ await self.start()
105
+
106
+ async def _connect(self) -> None:
107
+ """Подключается к вебсокету и настраивает соединение."""
108
+ self._logger.debug(f"Establishing connection with {self._url}")
109
+ async for conn in websockets.connect(uri=self._url, **self._generate_ws_kwargs()):
110
+ try:
111
+ self._logger.info(f"Websocket connection was established to {self._url}")
112
+ await self._after_connect(conn)
113
+
114
+ # Цикл получения сообщений
115
+ while self._running:
116
+ message = await conn.recv()
117
+ await self._handle_message(message)
118
+
119
+ except websockets.exceptions.ConnectionClosed as e:
120
+ self._logger.error(f"Websocket connection was closed unexpectedly: {e}")
121
+ except Exception as e:
122
+ self._logger.error(f"Unexpected error in websosocket connection: {e}")
123
+ finally:
124
+ # Делаем реконнект только если вебсокет активен, иначе выходим из итератора
125
+ if self._running:
126
+ await asyncio.sleep(self._reconnect_timeout)
127
+ await self._after_disconnect()
128
+ else:
129
+ return # Выходим из итератора, если вебсокет уже выключен
130
+
131
+ async def _handle_message(self, message: str | bytes) -> None:
132
+ """Обрабатывает входящее сообщение вебсокета."""
133
+ try:
134
+ # Обновленяем время последнего сообщения
135
+ self._last_message_time = time.monotonic()
136
+
137
+ # Ложим сообщение в очередь, предварительно его сериализуя
138
+ decoded_message = self._decoder.decode(message)
139
+ await self._queue.put(decoded_message)
140
+
141
+ # Проверяем размер очереди сообщений и выбрасываем ошибку, если он превышает максимальный размер
142
+ self._check_queue_size()
143
+ except QueueOverflowError:
144
+ self._logger.error("Message queue is overflow")
145
+ except orjson.JSONDecodeError as e:
146
+ if message in ["ping", "pong"]:
147
+ self._logger.debug(f"Received ping message: {message}")
148
+ else:
149
+ self._logger.error(f"Failed to decode JSON message: {message}, error: {e}")
150
+ except Exception as e:
151
+ self._logger.error(f"Unexpected error: {e}")
152
+
153
+ def _check_queue_size(self) -> None:
154
+ """Проверяет размер очереди и выбрасывает ошибку при переполнении."""
155
+ qsize = self._queue.qsize()
156
+ if qsize >= self.MAX_QUEUE_SIZE:
157
+ raise QueueOverflowError(f"Message queue is overflow: {qsize}")
158
+
159
+ async def _after_connect(self, conn: ClientConnection) -> None:
160
+ """Вызывается после установки соединения."""
161
+ # Подписываемся на топики
162
+ await self._send_subscribe_messages(conn)
163
+
164
+ # Обновленяем время последнего сообщения перед каждым подключением
165
+ self._last_message_time = time.monotonic()
166
+
167
+ # Запускам задачу для кастомного пинг сообщения
168
+ if self._ping_message:
169
+ self._tasks.append(asyncio.create_task(self._custom_ping_task(conn)))
170
+
171
+ # Запускаем healthcheck
172
+ if self._no_message_reconnect_timeout:
173
+ self._tasks.append(asyncio.create_task(self._healthcheck_task()))
174
+
175
+ # Запускаем воркеров
176
+ for _ in range(self._worker_count):
177
+ task = asyncio.create_task(self._worker())
178
+ self._tasks.append(task)
179
+
180
+ async def _after_disconnect(self) -> None:
181
+ """Вызывается после отключения от вебсокета."""
182
+ current_task = asyncio.current_task()
183
+
184
+ # Останавливаем воркеров, исключая задачу, которая уже выполняет остановку
185
+ tasks_to_wait: list[asyncio.Task] = []
186
+ for task in self._tasks:
187
+ if task is current_task:
188
+ continue
189
+
190
+ task.cancel()
191
+ tasks_to_wait.append(task)
192
+
193
+ # Дожидаемся завершения задач (в т.ч. воркеров)
194
+ if tasks_to_wait:
195
+ results = await asyncio.gather(*tasks_to_wait, return_exceptions=True)
196
+ for task_result in results:
197
+ if isinstance(task_result, asyncio.CancelledError):
198
+ continue
199
+ if isinstance(task_result, Exception):
200
+ self._logger.warning(f"Worker raised during shutdown: {task_result}")
201
+
202
+ self._tasks.clear()
203
+
204
+ # Очистить очередь уже безопасно, после остановки воркеров
205
+ self._queue = asyncio.Queue()
206
+
207
+ async def _send_subscribe_messages(self, conn: ClientConnection) -> None:
208
+ """Отправляет сообщения с подпиской на топики, если нужно."""
209
+ for message in self._subscription_messages:
210
+ await conn.send(message)
211
+ self._logger.debug(f"Sent subscribe message: {message}")
212
+
213
+ async def _worker(self) -> None:
214
+ """Обрабатывает сообщения из очереди."""
215
+ while self._running:
216
+ try:
217
+ data = await self._queue.get() # Получаем сообщение
218
+ await self._callback(data) # Передаем в callback
219
+ self._queue.task_done()
220
+ except asyncio.exceptions.CancelledError:
221
+ break
222
+ except Exception as e:
223
+ self._logger.error(f"Error({type(e)}) while processing message: {e}")
224
+
225
+ def _generate_ws_kwargs(self) -> dict:
226
+ """Генерирует аргументы для запуска вебсокета."""
227
+ ws_kwargs = {}
228
+ if self._ping_interval:
229
+ ws_kwargs["ping_interval"] = self._ping_interval
230
+ return ws_kwargs
231
+
232
+ async def _custom_ping_task(self, conn: ClientConnection) -> None:
233
+ """Периодически отправляет пользовательский ping."""
234
+ while self._running and self._ping_message:
235
+ try:
236
+ if isinstance(self._ping_message, Callable):
237
+ ping_message = self._ping_message()
238
+ else:
239
+ ping_message = self._ping_message
240
+ await conn.send(ping_message)
241
+ self._logger.debug(f"Sent ping message: {ping_message}")
242
+ except Exception as e:
243
+ self._logger.error(f"Error sending ping: {e}")
244
+ return
245
+ await asyncio.sleep(self._ping_interval)
246
+
247
+ async def _healthcheck_task(self) -> None:
248
+ """Следит за таймаутом получения сообщений."""
249
+ if not self._no_message_reconnect_timeout:
250
+ return
251
+
252
+ while self._running:
253
+ if time.monotonic() - self._last_message_time > self._no_message_reconnect_timeout:
254
+ self._logger.error("Websocket is not responding, restarting...")
255
+ await self.restart()
256
+ return
257
+ await asyncio.sleep(1)
258
+
259
+ def __repr__(self) -> str:
260
+ """Репрезентация вебсокета."""
261
+ return f"<Websocket(url={self._url[:15]}...)>"
@@ -0,0 +1,27 @@
1
+ """Пакет, содержащий реализации клиентов и менеджеров для работы с биржей Binance."""
2
+
3
+ __all__ = [
4
+ "Client",
5
+ "UniClient",
6
+ "UserWebsocket",
7
+ "WebsocketManager",
8
+ "UniWebsocketManager",
9
+ "ExchangeInfo",
10
+ ]
11
+
12
+ from .client import Client
13
+ from .exchange_info import ExchangeInfo
14
+ from .uni_client import UniClient
15
+ from .uni_websocket_manager import UniWebsocketManager
16
+ from .user_websocket import UserWebsocket
17
+ from .websocket_manager import WebsocketManager
18
+
19
+
20
+ async def load_exchange_info() -> None:
21
+ """Загружает информацию о бирже Binance."""
22
+ await ExchangeInfo.load_exchange_info()
23
+
24
+
25
+ async def start_exchange_info(parse_interval_seconds: int = 60 * 60) -> None:
26
+ """Запускает процесс обновления информации о бирже Binance."""
27
+ await ExchangeInfo.start(parse_interval_seconds)