maxapi-python 1.1.20__py3-none-any.whl → 1.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.1.20
3
+ Version: 1.2.1
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/ink-developer/PyMax
6
6
  Project-URL: Repository, https://github.com/ink-developer/PyMax
@@ -16,6 +16,7 @@ Requires-Dist: aiofiles>=24.1.0
16
16
  Requires-Dist: aiohttp>=3.12.15
17
17
  Requires-Dist: lz4>=4.4.4
18
18
  Requires-Dist: msgpack>=1.1.1
19
+ Requires-Dist: qrcode>=8.2
19
20
  Requires-Dist: sqlmodel>=0.0.24
20
21
  Requires-Dist: websockets>=15.0
21
22
  Description-Content-Type: text/markdown
@@ -35,6 +36,12 @@ Description-Content-Type: text/markdown
35
36
  <img src="https://img.shields.io/badge/packaging-uv-D7FF64.svg" alt="Packaging">
36
37
  </p>
37
38
 
39
+ > [!IMPORTANT]
40
+ > (20.12.25) Из за резкого изменения апи большая часть библиотеки не работает.
41
+ Смотрите [новость](https://t.me/pymax_news/111)
42
+ >
43
+ > P.s добавил логин по qr в dev/1.2.1
44
+
38
45
  ---
39
46
  > ⚠️ **Дисклеймер**
40
47
  >
@@ -79,65 +86,47 @@ uv add -U maxapi-python
79
86
 
80
87
  ```python
81
88
  import asyncio
89
+
82
90
  from pymax import MaxClient, Message
91
+ from pymax.filters import Filters
92
+
93
+ client = MaxClient(
94
+ phone="+1234567890",
95
+ work_dir="cache", # директория для сессий
96
+ )
97
+
83
98
 
84
- # Инициализация клиента
85
- phone = "+1234567890"
86
- client = MaxClient(phone=phone, work_dir="cache")
99
+ # Обработка входящих сообщений
100
+ @client.on_message(Filters.chat(0)) # фильтр по ID чата
101
+ async def on_message(msg: Message) -> None:
102
+ print(f"[{msg.sender}] {msg.text}")
103
+
104
+ await client.send_message(
105
+ chat_id=msg.chat_id,
106
+ text="Привет, я бот на PyMax!",
107
+ )
108
+
109
+ await client.add_reaction(
110
+ chat_id=msg.chat_id,
111
+ message_id=str(msg.id),
112
+ reaction="👍",
113
+ )
87
114
 
88
- # Обработчик входящих сообщений
89
- @client.on_message()
90
- async def handle_message(message: Message) -> None:
91
- print(f"{message.sender}: {message.text}")
92
115
 
93
- # Обработчик запуска клиента
94
116
  @client.on_start
95
- async def handle_start() -> None:
96
- print("Клиент запущен")
117
+ async def on_start() -> None:
118
+ print(f"Клиент запущен. Ваш ID: {client.me.id}")
97
119
 
98
- # Получение истории сообщений
120
+ # Получение истории
99
121
  history = await client.fetch_history(chat_id=0)
100
- if history:
101
- for message in history:
102
- user = await client.get_user(message.sender)
103
- if user:
104
- print(f"{user.names[0].name}: {message.text}")
105
-
106
- async def main() -> None:
107
- await client.start()
108
-
109
- # Работа с чатами
110
- for chat in client.chats:
111
- print(f"Чат: {chat.title}")
112
-
113
- # Отправка сообщения
114
- message = await client.send_message(
115
- "Привет от PyMax!",
116
- chat.id,
117
- notify=True
118
- )
119
-
120
- # Редактирование сообщения
121
- await asyncio.sleep(2)
122
- await client.edit_message(
123
- chat.id,
124
- message.id,
125
- "Привет от PyMax! (отредактировано)"
126
- )
127
-
128
- # Удаление сообщения
129
- await asyncio.sleep(2)
130
- await client.delete_message(chat.id, [message.id], for_me=False)
131
-
132
- # Работа с диалогами
133
- for dialog in client.dialogs:
134
- print(f"Диалог: {dialog.last_message.text}")
135
-
136
- # Работа с каналами
137
- for channel in client.channels:
138
- print(f"Канал: {channel.title}")
139
-
140
- await client.close()
122
+ print("Последние сообщения из чата 0:")
123
+ for m in history:
124
+ print(f"- {m.text}")
125
+
126
+
127
+ async def main():
128
+ await client.start() # подключение и авторизация
129
+
141
130
 
142
131
  if __name__ == "__main__":
143
132
  asyncio.run(main())
@@ -145,7 +134,8 @@ if __name__ == "__main__":
145
134
 
146
135
  ## Документация
147
136
 
148
- [WIP](https://ink-developer.github.io/PyMax)
137
+ [GitHub Pages](https://maxapiteam.github.io/PyMax/)
138
+ [DeepWiki](https://deepwiki.com/MaxApiTeam/PyMax)
149
139
 
150
140
  ## Лицензия
151
141
 
@@ -0,0 +1,32 @@
1
+ pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
+ pymax/core.py,sha256=Y4sXaUEtK4pVyYiKf5folyhW_xMFUJ3W-5Hc33b8v0E,14892
3
+ pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
4
+ pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
+ pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
6
+ pymax/filters.py,sha256=gSHPJ1Vi37HKPxf0jRRv9Q3iGwhiQjw1MGrCaouqHzs,4325
7
+ pymax/formatter.py,sha256=RJ_5VbY7Li8UM3xL1AvcXo8v1iYnY8GvDDkreaFqtnY,860
8
+ pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
9
+ pymax/interfaces.py,sha256=wKF1z1QRw8LcjvM9rzSHWXTK6gPb6sDt2UGiQLvyMf8,8790
10
+ pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
+ pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
+ pymax/payloads.py,sha256=-GEJVXXlmJiFSTX4ToVNzmSZSrvSRe-BLOwYyRxGkWY,7280
13
+ pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
14
+ pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
15
+ pymax/mixins/auth.py,sha256=e90vIpEOwAjUxgYMYaG7R6jR_5t9rKsei_mTBQUirL4,14716
16
+ pymax/mixins/channel.py,sha256=W52YnBay1sUYXxF9oAWsz44ZUh_s45jSvKmAyjTbULM,5357
17
+ pymax/mixins/group.py,sha256=LqI1QHmZlmtuQ0-4H1MrNeBV-O9SMDMfHT9f4B_2poE,15189
18
+ pymax/mixins/handler.py,sha256=ETnI8fA386LYJGjWtUhhWzQHREUA78di1aO1oWwtscA,12523
19
+ pymax/mixins/message.py,sha256=AznKKmTMxdzsYl8IecT43RjWpGvlQM85GzSNGFbI8BA,33279
20
+ pymax/mixins/scheduler.py,sha256=rcMfgfZnzu5V6MkcCg6uRgbi-jkc7UyqOjemulydWbc,964
21
+ pymax/mixins/self.py,sha256=Be5L64eNYylGM-NmoxFpQZv1ohsC1Dx_Cs3Om__V96s,6976
22
+ pymax/mixins/socket.py,sha256=tdHgd1NwWoEZhHCDd74XLOHFKUq-rladxhXV8Z_-APU,22860
23
+ pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
24
+ pymax/mixins/user.py,sha256=RSZd4t-aq8P2k3cVzNVWBkUf-_xTWILrBzwxLRgk1pw,9450
25
+ pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
26
+ pymax/mixins/websocket.py,sha256=m2swhSHIcFG6iABAik_oWxIpHfr0sxZ74I6VRU-iVO8,17809
27
+ pymax/static/constant.py,sha256=-qJz65V_ESagy7DYHRj3QsfKwyAyVofWzZh0AqBJtXo,1112
28
+ pymax/static/enum.py,sha256=Hk0e6zSbGOJC_9Aw7gNXX3hcavnjzQfDyr8vjW22cFo,4648
29
+ maxapi_python-1.2.1.dist-info/METADATA,sha256=JU86mxRn8zdfsRGWFnNvJ6ea6KHKmmADCvDWvuvIZb4,5855
30
+ maxapi_python-1.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ maxapi_python-1.2.1.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
32
+ maxapi_python-1.2.1.dist-info/RECORD,,
pymax/core.py CHANGED
@@ -1,23 +1,24 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import contextlib
3
5
  import logging
4
6
  import socket
5
7
  import ssl
6
8
  import time
7
- import traceback
8
9
  from collections.abc import Awaitable
9
10
  from pathlib import Path
10
11
  from typing import TYPE_CHECKING, Any, Literal
12
+ from uuid import UUID
11
13
 
12
- from typing_extensions import Self, override
14
+ from typing_extensions import override
13
15
 
14
16
  from .crud import Database
15
17
  from .exceptions import (
16
18
  InvalidPhoneError,
17
19
  SocketNotConnectedError,
18
- WebSocketNotConnectedError,
19
20
  )
20
- from .formatter import ColoredFormatter
21
+ from .interfaces import BaseClient
21
22
  from .mixins import ApiMixin, SocketMixin, WebSocketMixin
22
23
  from .payloads import UserAgentPayload
23
24
  from .static.constant import (
@@ -27,45 +28,53 @@ from .static.constant import (
27
28
  )
28
29
 
29
30
  if TYPE_CHECKING:
30
- from collections.abc import Awaitable, Callable
31
- from typing import Any
31
+ from collections.abc import Callable
32
32
 
33
33
  import websockets
34
34
 
35
- from .filters import Filter
35
+ from pymax.filters import BaseFilter
36
+
37
+ from .filters import Filters
36
38
  from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
37
39
 
38
40
 
39
41
  logger = logging.getLogger(__name__)
40
42
 
41
43
 
42
- class MaxClient(ApiMixin, WebSocketMixin):
44
+ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
43
45
  """
44
46
  Основной клиент для работы с WebSocket API сервиса Max.
45
47
 
46
-
47
- Args:
48
- phone (str): Номер телефона для авторизации.
49
- uri (str, optional): URI WebSocket сервера. По умолчанию Constants.WEBSOCKET_URI.value.
50
- work_dir (str, optional): Рабочая директория для хранения базы данных. По умолчанию ".".
51
- logger (logging.Logger | None): Пользовательский логгер. Если не передан — используется
52
- логгер модуля с именем f"{__name__}.MaxClient".
53
- headers (UserAgentPayload): Заголовки для подключения к WebSocket.
54
- token (str | None, optional): Токен авторизации. Если не передан, будет выполнен
55
- процесс логина по номеру телефона.
56
- host (str, optional): Хост API сервера. По умолчанию Constants.HOST.value.
57
- port (int, optional): Порт API сервера. По умолчанию Constants.PORT.value.
58
- registration (bool, optional): Флаг регистрации нового пользователя. По умолчанию False.
59
- first_name (str, optional): Имя пользователя для регистрации. Требуется, если registration=True.
60
- last_name (str | None, optional): Фамилия пользователя для регистрации.
61
- send_fake_telemetry (bool, optional): Флаг отправки фейковой телеметрии. По умолчанию True.
62
- proxy (str | Literal[True] | None, optional): Прокси для подключения к WebSocket.
63
- (См. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
64
- reconnect (bool, optional): Флаг автоматического переподключения при потере соединения. По умолчанию True.
65
-
66
-
67
- Raises:
68
- InvalidPhoneError: Если формат номера телефона неверный.
48
+ :param phone: Номер телефона для авторизации.
49
+ :type phone: str
50
+ :param uri: URI WebSocket сервера.
51
+ :type uri: str, optional
52
+ :param work_dir: Рабочая директория для хранения базы данных.
53
+ :type work_dir: str, optional
54
+ :param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
55
+ :type logger: logging.Logger | None
56
+ :param headers: Заголовки для подключения к WebSocket.
57
+ :type headers: UserAgentPayload
58
+ :param token: Токен авторизации. Если не передан, будет выполнен процесс логина по номеру телефона.
59
+ :type token: str | None, optional
60
+ :param host: Хост API сервера.
61
+ :type host: str, optional
62
+ :param port: Порт API сервера.
63
+ :type port: int, optional
64
+ :param registration: Флаг регистрации нового пользователя.
65
+ :type registration: bool, optional
66
+ :param first_name: Имя пользователя для регистрации. Требуется, если registration=True.
67
+ :type first_name: str, optional
68
+ :param last_name: Фамилия пользователя для регистрации.
69
+ :type last_name: str | None, optional
70
+ :param send_fake_telemetry: Флаг отправки фейковой телеметрии.
71
+ :type send_fake_telemetry: bool, optional
72
+ :param proxy: Прокси для подключения к WebSocket (см. https://websockets.readthedocs.io/en/stable/topics/proxies.html).
73
+ :type proxy: str | Literal[True] | None, optional
74
+ :param reconnect: Флаг автоматического переподключения при потере соединения.
75
+ :type reconnect: bool, optional
76
+
77
+ :raises InvalidPhoneError: Если формат номера телефона неверный.
69
78
  """
70
79
 
71
80
  def __init__(
@@ -82,6 +91,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
82
91
  registration: bool = False,
83
92
  first_name: str = "",
84
93
  last_name: str | None = None,
94
+ device_id: UUID | None = None,
85
95
  logger: logging.Logger | None = None,
86
96
  reconnect: bool = True,
87
97
  reconnect_delay: float = 1.0,
@@ -127,7 +137,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
127
137
  self._circuit_breaker: bool = False
128
138
  self._last_error_time: float = 0.0
129
139
 
130
- self._device_id = self._database.get_device_id()
140
+ self._device_id = device_id if device_id is not None else self._database.get_device_id()
131
141
  self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
132
142
 
133
143
  self._token = self._database.get_auth_token() or token
@@ -138,19 +148,20 @@ class MaxClient(ApiMixin, WebSocketMixin):
138
148
  self._current_screen: str = "chats_list_tab"
139
149
 
140
150
  self._on_message_handlers: list[
141
- tuple[Callable[[Message], Any], Filter | None]
151
+ tuple[Callable[[Message], Any], BaseFilter[Message] | None]
142
152
  ] = []
143
153
  self._on_message_edit_handlers: list[
144
- tuple[Callable[[Message], Any], Filter | None]
154
+ tuple[Callable[[Message], Any], BaseFilter[Message] | None]
145
155
  ] = []
146
156
  self._on_message_delete_handlers: list[
147
- tuple[Callable[[Message], Any], Filter | None]
157
+ tuple[Callable[[Message], Any], BaseFilter[Message] | None]
148
158
  ] = []
149
159
  self._on_start_handler: Callable[[], Any | Awaitable[Any]] | None = None
150
- self._on_reaction_change_handlers: list[
151
- tuple[Callable[[str, int, ReactionInfo], Any]]
152
- ] = []
153
- self._on_chat_update_handlers: list[tuple[Callable[[Chat], Any]]] = []
160
+ self._on_stop_handler: Callable[[], Any | Awaitable[Any]] | None = None
161
+ self._on_reaction_change_handlers: list[Callable[[str, int, ReactionInfo], Any]] = []
162
+ self._on_chat_update_handlers: list[Callable[[Chat], Any | Awaitable[Any]]] = []
163
+ self._on_raw_receive_handlers: list[Callable[[dict[str, Any]], Any | Awaitable[Any]]] = []
164
+ self._scheduled_tasks: list[tuple[Callable[[], Any | Awaitable[Any]], float]] = []
154
165
 
155
166
  self._ssl_context = ssl.create_default_context()
156
167
  self._ssl_context.set_ciphers("DEFAULT")
@@ -168,37 +179,18 @@ class MaxClient(ApiMixin, WebSocketMixin):
168
179
  self._work_dir,
169
180
  )
170
181
 
171
- def _setup_logger(self) -> None:
172
- if not self.logger.handlers:
173
- if not self.logger.level:
174
- self.logger.setLevel(logging.INFO)
175
- handler = logging.StreamHandler()
176
- formatter = ColoredFormatter(
177
- "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
178
- datefmt="%Y-%m-%d %H:%M:%S",
179
- )
180
- handler.setFormatter(formatter)
181
- self.logger.addHandler(handler)
182
-
183
182
  async def _wait_forever(self) -> None:
184
183
  try:
185
184
  await self.ws.wait_closed()
186
185
  except asyncio.CancelledError:
187
186
  self.logger.debug("wait_closed cancelled")
188
187
 
189
- async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
190
- """
191
- Безопасно выполняет пользовательскую корутину.
192
- Логирует traceback, но не роняет event loop.
188
+ async def close(self) -> None:
193
189
  """
194
- try:
195
- return await coro
196
- except Exception as e:
197
- self.logger.error(
198
- f"Unhandled exception in {context}: {e}\n{traceback.format_exc()}"
199
- )
190
+ Закрывает клиент и освобождает ресурсы.
200
191
 
201
- async def close(self) -> None:
192
+ :return: None
193
+ """
202
194
  try:
203
195
  self.logger.info("Closing client")
204
196
  if self._recv_task:
@@ -220,100 +212,46 @@ class MaxClient(ApiMixin, WebSocketMixin):
220
212
  except Exception:
221
213
  self.logger.exception("Error closing client")
222
214
 
223
- @override
224
- def _create_safe_task(
225
- self, coro: Awaitable[Any], *, name: str | None = None
226
- ) -> asyncio.Task[Any | None]:
227
- async def runner():
228
- try:
229
- return await coro
230
- except asyncio.CancelledError:
231
- raise
232
- except Exception as e:
233
- self.logger.exception(
234
- f"Unhandled exception in task {name or coro}: {e}",
235
- exc_info=e,
236
- )
237
- return None
238
-
239
- task = asyncio.create_task(runner(), name=name)
240
- self._background_tasks.add(task)
241
- return task
242
-
243
215
  async def _post_login_tasks(self, sync: bool = True) -> None:
244
216
  if sync:
245
217
  await self._sync()
246
218
 
247
- if self._on_start_handler:
248
- self.logger.debug("Calling on_start handler")
249
- result = self._on_start_handler()
250
- if asyncio.iscoroutine(result):
251
- await self._safe_execute(result, context="on_start handler")
252
-
219
+ self.logger.debug("is_connected=%s before starting ping", self.is_connected)
253
220
  ping_task = asyncio.create_task(self._send_interactive_ping())
254
221
  ping_task.add_done_callback(self._log_task_exception)
255
222
  self._background_tasks.add(ping_task)
256
223
 
224
+ start_scheduled_task = asyncio.create_task(self._start_scheduled_tasks())
225
+ start_scheduled_task.add_done_callback(self._log_task_exception)
226
+
257
227
  if self._send_fake_telemetry:
258
228
  telemetry_task = asyncio.create_task(self._start())
259
229
  telemetry_task.add_done_callback(self._log_task_exception)
260
230
  self._background_tasks.add(telemetry_task)
261
231
 
262
- async def _cleanup_client(self) -> None:
263
- for task in list(self._background_tasks):
264
- task.cancel()
265
- try:
266
- await task
267
- except asyncio.CancelledError:
268
- pass
269
- except Exception:
270
- self.logger.debug(
271
- "Background task raised during cancellation", exc_info=True
272
- )
273
- self._background_tasks.discard(task)
274
-
275
- if self._recv_task:
276
- self._recv_task.cancel()
277
- with contextlib.suppress(asyncio.CancelledError):
278
- await self._recv_task
279
- self._recv_task = None
280
-
281
- if self._outgoing_task:
282
- self._outgoing_task.cancel()
283
- with contextlib.suppress(asyncio.CancelledError):
284
- await self._outgoing_task
285
- self._outgoing_task = None
286
-
287
- for fut in self._pending.values():
288
- if not fut.done():
289
- fut.set_exception(WebSocketNotConnectedError)
290
- self._pending.clear()
291
-
292
- if self._ws:
293
- try:
294
- await self._ws.close()
295
- except Exception:
296
- self.logger.debug("Error closing ws during cleanup", exc_info=True)
297
- self._ws = None
298
-
299
- self.is_connected = False
300
- self.logger.info("Client start() cleaned up")
232
+ if self._on_start_handler:
233
+ self.logger.debug("Calling on_start handler")
234
+ result = self._on_start_handler()
235
+ if asyncio.iscoroutine(result):
236
+ await self._safe_execute(result, context="on_start handler")
301
237
 
302
- async def login_with_code(
303
- self, temp_token: str, code: str, start: bool = False
304
- ) -> None:
238
+ async def login_with_code(self, temp_token: str, code: str, start: bool = False) -> None:
305
239
  """
306
240
  Завершает кастомный login flow: отправляет код, сохраняет токен и запускает пост-логин задачи.
307
241
 
308
- Args:
309
- temp_token (str): Временный токен, полученный из request_code()
310
- code (str): Код, введённый пользователем
311
-
312
- Returns:
313
- str: Токен для входа
242
+ :param temp_token: Временный токен, полученный из request_code.
243
+ :type temp_token: str
244
+ :param code: Код верификации (6 цифр).
245
+ :type code: str
246
+ :param start: Флаг запуска пост-логин задач и ожидания навсегда. Если False, только сохраняет токен.
247
+ :type start: bool, optional
248
+ :return: None
249
+ :rtype: None
314
250
  """
315
251
  resp = await self._send_code(code, temp_token)
316
252
  token = resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
253
+ if not token:
254
+ raise ValueError("Login response did not contain tokenAttrs.LOGIN.token")
317
255
  self._token = token
318
256
  self._database.update_auth_token(self._device_id, token)
319
257
  if start:
@@ -336,6 +274,9 @@ class MaxClient(ApiMixin, WebSocketMixin):
336
274
  Запускает клиент, подключается к WebSocket, авторизует
337
275
  пользователя (если нужно) и запускает фоновый цикл.
338
276
  Теперь включает безопасный reconnect-loop, если self.reconnect=True.
277
+
278
+ :return: None
279
+ :rtype: None
339
280
  """
340
281
 
341
282
  while True:
@@ -353,8 +294,8 @@ class MaxClient(ApiMixin, WebSocketMixin):
353
294
 
354
295
  if self._token is None:
355
296
  await self._login()
356
- else:
357
- await self._sync()
297
+
298
+ await self._sync()
358
299
 
359
300
  await self._post_login_tasks(sync=False)
360
301
 
@@ -376,18 +317,6 @@ class MaxClient(ApiMixin, WebSocketMixin):
376
317
  self.logger.info("Reconnect enabled — restarting client")
377
318
  await asyncio.sleep(self.reconnect_delay)
378
319
 
379
- async def idle(self):
380
- await asyncio.Event().wait()
381
-
382
- async def __aenter__(self) -> Self:
383
- self._create_safe_task(self.start(), name="start")
384
- while not self.is_connected:
385
- await asyncio.sleep(0.05)
386
- return self
387
-
388
- async def __aexit__(self, exc_type, exc, tb) -> None:
389
- await self.close()
390
-
391
320
 
392
321
  class SocketMaxClient(SocketMixin, MaxClient):
393
322
  @override
@@ -402,12 +331,6 @@ class SocketMaxClient(SocketMixin, MaxClient):
402
331
 
403
332
  @override
404
333
  async def _cleanup_client(self):
405
- """
406
- Socket-specific cleanup: cancel background tasks, set pending futures
407
- exceptions to SocketNotConnectedError, and close socket.
408
- """
409
- from .exceptions import SocketNotConnectedError
410
-
411
334
  for task in list(self._background_tasks):
412
335
  task.cancel()
413
336
  try:
pymax/crud.py CHANGED
@@ -31,9 +31,8 @@ class Database:
31
31
 
32
32
  def get_device_id(self) -> UUID:
33
33
  with self.get_session() as session:
34
- device_id = cast(
35
- UUID | None, session.exec(select(Auth.device_id)).first()
36
- )
34
+ device_id = session.exec(select(Auth.device_id)).first()
35
+
37
36
  if device_id is None:
38
37
  auth = Auth()
39
38
  session.add(auth)
@@ -51,9 +50,7 @@ class Database:
51
50
 
52
51
  def update_auth_token(self, device_id: UUID, token: str) -> None:
53
52
  with self.get_session() as session:
54
- auth = session.exec(
55
- select(Auth).where(Auth.device_id == device_id)
56
- ).first()
53
+ auth = session.exec(select(Auth).where(Auth.device_id == device_id)).first()
57
54
  if auth:
58
55
  auth.token = token
59
56
  session.add(auth)
@@ -86,7 +83,6 @@ class Database:
86
83
  with self.get_session() as session:
87
84
  rows = session.exec(select(Auth)).all()
88
85
  if not rows:
89
- # Create default Auth with device type from enum
90
86
  auth = Auth(device_type=DeviceType.WEB.value)
91
87
  session.add(auth)
92
88
  session.commit()