maxapi-python 1.1.19__py3-none-any.whl → 1.1.20__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.19
3
+ Version: 1.1.20
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
@@ -0,0 +1,31 @@
1
+ pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
+ pymax/core.py,sha256=LqX56a5BagUYl1vpB55Y1pLZQdMoC86t6mIQVlkVByo,17322
3
+ pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
4
+ pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
+ pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
6
+ pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
7
+ pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
8
+ pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
9
+ pymax/interfaces.py,sha256=Re8o5N7FSQ-5OgVlK4-WBltX27GheEbfFjoIYl9_u6I,3723
10
+ pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
+ pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
+ pymax/payloads.py,sha256=cEXY_cVL6SPyhoFTTZnn7dyUx9MMdtNT5SuQSQtL4rg,6983
13
+ pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
14
+ pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
15
+ pymax/mixins/auth.py,sha256=Emv-0WVB_orwv9L_V5gAHfp-VYVaVcbW6AlclW_K6W4,6731
16
+ pymax/mixins/channel.py,sha256=7c8GANyxZuNbIHNBVcPAmMa1qqA1IRf9cGPBS1oK_q4,5159
17
+ pymax/mixins/group.py,sha256=XWXNWluCvq4KkZWqv4sxLpzkXfH33U1yEP20_ZFtSM4,10624
18
+ pymax/mixins/handler.py,sha256=ZuYX8wSgNXJoSMArcwyHvY_bL9A7X0AXnAOz22ATA3k,5897
19
+ pymax/mixins/message.py,sha256=wYvkMPE9ORCSFd_9J-6ltf__4ELG_zaZ_Uey4rmCzHg,25460
20
+ pymax/mixins/self.py,sha256=3BdHfUyqw3dn3ctJX9_hilP1jOaTaunstZ7nH8Y_xcU,5436
21
+ pymax/mixins/socket.py,sha256=GEOscQKKC48bOAXXiDLoz8GusCLtjdxQnB5AK5xJbms,26997
22
+ pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
23
+ pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
24
+ pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
25
+ pymax/mixins/websocket.py,sha256=toiXt9qxx6yTgnWJdEOeNfp414MD4zmbHp1qVgVkjnY,19850
26
+ pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
27
+ pymax/static/enum.py,sha256=ddw5SEVfRb2J9TXOa5IGhssNd-7RyKfwZBKx_UionEM,4562
28
+ maxapi_python-1.1.20.dist-info/METADATA,sha256=9yhRv1m8PbJJhnUdloacxlK0jAE0veq6zfXDP-Ok5nk,6245
29
+ maxapi_python-1.1.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
+ maxapi_python-1.1.20.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
31
+ maxapi_python-1.1.20.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pymax/core.py CHANGED
@@ -7,12 +7,16 @@ import time
7
7
  import traceback
8
8
  from collections.abc import Awaitable
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Literal, Self
10
+ from typing import TYPE_CHECKING, Any, Literal
11
11
 
12
- from typing_extensions import override
12
+ from typing_extensions import Self, override
13
13
 
14
14
  from .crud import Database
15
- from .exceptions import InvalidPhoneError, WebSocketNotConnectedError
15
+ from .exceptions import (
16
+ InvalidPhoneError,
17
+ SocketNotConnectedError,
18
+ WebSocketNotConnectedError,
19
+ )
16
20
  from .formatter import ColoredFormatter
17
21
  from .mixins import ApiMixin, SocketMixin, WebSocketMixin
18
22
  from .payloads import UserAgentPayload
@@ -176,13 +180,13 @@ class MaxClient(ApiMixin, WebSocketMixin):
176
180
  handler.setFormatter(formatter)
177
181
  self.logger.addHandler(handler)
178
182
 
179
- async def _wait_forever(self):
183
+ async def _wait_forever(self) -> None:
180
184
  try:
181
185
  await self.ws.wait_closed()
182
186
  except asyncio.CancelledError:
183
187
  self.logger.debug("wait_closed cancelled")
184
188
 
185
- async def _safe_execute(self, coro, *, context: str = "unknown"):
189
+ async def _safe_execute(self, coro, *, context: str = "unknown") -> Any:
186
190
  """
187
191
  Безопасно выполняет пользовательскую корутину.
188
192
  Логирует traceback, но не роняет event loop.
@@ -255,7 +259,7 @@ class MaxClient(ApiMixin, WebSocketMixin):
255
259
  telemetry_task.add_done_callback(self._log_task_exception)
256
260
  self._background_tasks.add(telemetry_task)
257
261
 
258
- async def _cleanup_client(self):
262
+ async def _cleanup_client(self) -> None:
259
263
  for task in list(self._background_tasks):
260
264
  task.cancel()
261
265
  try:
@@ -356,7 +360,6 @@ class MaxClient(ApiMixin, WebSocketMixin):
356
360
 
357
361
  await self._wait_forever()
358
362
  self.logger.info("WebSocket closed (wait_forever exited)")
359
-
360
363
  except Exception as e:
361
364
  self.logger.exception("Client start iteration failed")
362
365
  raise e
@@ -396,3 +399,51 @@ class SocketMaxClient(SocketMixin, MaxClient):
396
399
  self.logger.debug("Socket recv_task cancelled")
397
400
  except Exception as e:
398
401
  self.logger.exception("Socket recv_task failed: %s", e)
402
+
403
+ @override
404
+ 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
+ for task in list(self._background_tasks):
412
+ task.cancel()
413
+ try:
414
+ await task
415
+ except asyncio.CancelledError:
416
+ pass
417
+ except Exception:
418
+ self.logger.debug(
419
+ "Background task raised during cancellation (socket)",
420
+ exc_info=True,
421
+ )
422
+ self._background_tasks.discard(task)
423
+
424
+ if self._recv_task:
425
+ self._recv_task.cancel()
426
+ with contextlib.suppress(asyncio.CancelledError):
427
+ await self._recv_task
428
+ self._recv_task = None
429
+
430
+ if self._outgoing_task:
431
+ self._outgoing_task.cancel()
432
+ with contextlib.suppress(asyncio.CancelledError):
433
+ await self._outgoing_task
434
+ self._outgoing_task = None
435
+
436
+ for fut in self._pending.values():
437
+ if not fut.done():
438
+ fut.set_exception(SocketNotConnectedError())
439
+ self._pending.clear()
440
+
441
+ if self._socket:
442
+ try:
443
+ self._socket.close()
444
+ except Exception:
445
+ self.logger.debug("Error closing socket during cleanup", exc_info=True)
446
+ self._socket = None
447
+
448
+ self.is_connected = False
449
+ self.logger.info("Client start() cleaned up (socket)")
pymax/files.py CHANGED
@@ -9,9 +9,7 @@ from typing_extensions import override
9
9
 
10
10
 
11
11
  class BaseFile(ABC):
12
- def __init__(
13
- self, url: str | None = None, path: str | None = None
14
- ) -> None:
12
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
15
13
  self.url = url
16
14
  self.path = path
17
15
 
@@ -47,9 +45,7 @@ class Photo(BaseFile):
47
45
  ".bmp",
48
46
  } # FIXME: костыль ✅
49
47
 
50
- def __init__(
51
- self, url: str | None = None, path: str | None = None
52
- ) -> None:
48
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
53
49
  super().__init__(url, path)
54
50
 
55
51
  def validate_photo(self) -> tuple[str, str] | None:
@@ -71,9 +67,7 @@ class Photo(BaseFile):
71
67
  mime_type = mimetypes.guess_type(self.url)[0]
72
68
 
73
69
  if not mime_type or not mime_type.startswith("image/"):
74
- raise ValueError(
75
- f"URL does not appear to be an image: {self.url}"
76
- )
70
+ raise ValueError(f"URL does not appear to be an image: {self.url}")
77
71
 
78
72
  return (extension[1:], mime_type)
79
73
  return None
@@ -84,15 +78,24 @@ class Photo(BaseFile):
84
78
 
85
79
 
86
80
  class Video(BaseFile):
81
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
82
+ self.file_name: str = ""
83
+ if path:
84
+ self.file_name = Path(path).name
85
+ elif url:
86
+ self.file_name = Path(url).name
87
+
88
+ if not self.file_name:
89
+ raise ValueError("Either url or path must be provided.")
90
+ super().__init__(url, path)
91
+
87
92
  @override
88
93
  async def read(self) -> bytes:
89
94
  return await super().read()
90
95
 
91
96
 
92
97
  class File(BaseFile):
93
- def __init__(
94
- self, url: str | None = None, path: str | None = None
95
- ) -> None:
98
+ def __init__(self, url: str | None = None, path: str | None = None) -> None:
96
99
  self.file_name: str = ""
97
100
  if path:
98
101
  self.file_name = Path(path).name
pymax/interfaces.py CHANGED
@@ -4,23 +4,24 @@ import ssl
4
4
  from abc import ABC, abstractmethod
5
5
  from collections.abc import Awaitable, Callable
6
6
  from logging import Logger
7
- from pathlib import Path
8
7
  from typing import TYPE_CHECKING, Any, Literal
9
8
 
10
- import websockets
11
-
12
- from .filters import Filter
13
9
  from .payloads import UserAgentPayload
14
10
  from .static.constant import DEFAULT_TIMEOUT
15
11
  from .static.enum import Opcode
16
12
  from .types import Channel, Chat, Dialog, Me, Message, User
17
13
 
18
14
  if TYPE_CHECKING:
15
+ from pathlib import Path
19
16
  from uuid import UUID
20
17
 
18
+ import websockets
19
+
20
+ from pymax import AttachType
21
21
  from pymax.types import ReactionInfo
22
22
 
23
23
  from .crud import Database
24
+ from .filters import Filter
24
25
 
25
26
 
26
27
  class ClientProtocol(ABC):
@@ -52,7 +53,10 @@ class ClientProtocol(ABC):
52
53
  self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
53
54
  self._recv_task: asyncio.Task[Any] | None = None
54
55
  self._incoming: asyncio.Queue[dict[str, Any]] | None = None
55
- self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
56
+ self._file_upload_waiters: dict[
57
+ int,
58
+ asyncio.Future[dict[str, Any]],
59
+ ] = {}
56
60
  self.user_agent = UserAgentPayload()
57
61
  self._outgoing: asyncio.Queue[dict[str, Any]] | None = None
58
62
  self._outgoing_task: asyncio.Task[Any] | None = None
pymax/mixins/auth.py CHANGED
@@ -139,7 +139,7 @@ class AuthMixin(ClientProtocol):
139
139
  self.logger.info("Starting registration flow")
140
140
 
141
141
  request_code_payload = await self.request_code(self.phone)
142
- temp_token = request_code_payload.get("token")
142
+ temp_token = request_code_payload
143
143
 
144
144
  if not temp_token or not isinstance(temp_token, str):
145
145
  self.logger.critical("Failed to request code: token missing")
pymax/mixins/channel.py CHANGED
@@ -67,11 +67,11 @@ class ChannelMixin(ClientProtocol):
67
67
  ) -> tuple[list[Member], int | None]:
68
68
  data = await self._send_and_wait(
69
69
  opcode=Opcode.CHAT_MEMBERS,
70
- payload=payload.model_dump(by_alias=True),
70
+ payload=payload.model_dump(by_alias=True, exclude_none=True),
71
71
  )
72
72
  response_payload = data.get("payload", {})
73
- if error := response_payload.get("error"):
74
- raise ResponseError(error)
73
+ if data.get("payload", {}).get("error"):
74
+ MixinsUtils.handle_error(data)
75
75
  marker = response_payload.get("marker")
76
76
  if isinstance(marker, str):
77
77
  marker = int(marker)
@@ -96,7 +96,7 @@ class ChannelMixin(ClientProtocol):
96
96
  async def load_members(
97
97
  self,
98
98
  chat_id: int,
99
- marker: int = DEFAULT_MARKER_VALUE,
99
+ marker: int | None = DEFAULT_MARKER_VALUE,
100
100
  count: int = DEFAULT_CHAT_MEMBERS_LIMIT,
101
101
  ) -> tuple[list[Member], int | None]:
102
102
  """
@@ -106,11 +106,11 @@ class ChannelMixin(ClientProtocol):
106
106
  chat_id (int): Идентификатор канала
107
107
  marker (int, optional): Маркер для пагинации. По умолчанию DEFAULT_MARKER_VALUE
108
108
  count (int, optional): Количество членов для загрузки. По умолчанию DEFAULT_CHAT_MEMBERS_LIMIT.
109
- Данное значение лучше не менять, так как веб-клиент загружает именно столько.
110
109
 
111
110
  Returns:
112
- list[Member]: Список участников канала
111
+ tuple[list[Member], int | None]: Список участников канала и маркер для следующей страницы
113
112
  """
113
+
114
114
  payload = GetGroupMembersPayload(chat_id=chat_id, marker=marker, count=count)
115
115
  return await self._query_members(payload)
116
116
 
pymax/mixins/group.py CHANGED
@@ -10,6 +10,7 @@ from pymax.payloads import (
10
10
  CreateGroupAttach,
11
11
  CreateGroupMessage,
12
12
  CreateGroupPayload,
13
+ GetChatInfoPayload,
13
14
  InviteUsersPayload,
14
15
  JoinChatPayload,
15
16
  RemoveUsersPayload,
@@ -258,3 +259,62 @@ class GroupMixin(ClientProtocol):
258
259
  raise Error("no_chat", "Chat data missing in response", "Chat Error")
259
260
 
260
261
  return chat
262
+
263
+ async def get_chats(self, chat_ids: list[int]) -> list[Chat]:
264
+ """
265
+ Получает информацию о группах по их ID
266
+
267
+ Args:
268
+ chat_ids (list[int]): Список идентификаторов групп.
269
+
270
+ Returns:
271
+ list[Chat]: Список объектов Chat.
272
+ """
273
+ missed_chat_ids = [
274
+ chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None
275
+ ]
276
+ if missed_chat_ids:
277
+ payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(
278
+ by_alias=True
279
+ )
280
+ else:
281
+ chats: list[Chat] = [
282
+ chat
283
+ for chat_id in chat_ids
284
+ if (chat := await self._get_chat(chat_id)) is not None
285
+ ]
286
+ return chats
287
+
288
+ data = await self._send_and_wait(opcode=Opcode.CHAT_INFO, payload=payload)
289
+
290
+ if data.get("payload", {}).get("error"):
291
+ MixinsUtils.handle_error(data)
292
+
293
+ chats_data = data["payload"].get("chats", [])
294
+ chats: list[Chat] = []
295
+ for chat_dict in chats_data:
296
+ chat = Chat.from_dict(chat_dict)
297
+ chats.append(chat)
298
+ cached_chat = await self._get_chat(chat.id)
299
+ if cached_chat is None:
300
+ self.chats.append(chat)
301
+ else:
302
+ idx = self.chats.index(cached_chat)
303
+ self.chats[idx] = chat
304
+
305
+ return chats
306
+
307
+ async def get_chat(self, chat_id: int) -> Chat:
308
+ """
309
+ Получает информацию о группе по ее ID
310
+
311
+ Args:
312
+ chat_id (int): Идентификатор группы.
313
+
314
+ Returns:
315
+ Chat: Объект Chat.
316
+ """
317
+ chats = await self.get_chats([chat_id])
318
+ if not chats:
319
+ raise Error("no_chat", "Chat not found in response", "Chat Error")
320
+ return chats[0]
pymax/mixins/handler.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from collections.abc import Awaitable, Callable
2
2
  from typing import Any
3
3
 
4
- from pymax.interfaces import ClientProtocol, Filter
4
+ from pymax.filters import Filter
5
+ from pymax.interfaces import ClientProtocol
5
6
  from pymax.types import Chat, Message, ReactionInfo
6
7
 
7
8
 
pymax/mixins/message.py CHANGED
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
2
  import time
3
+ from http import HTTPStatus
3
4
 
4
5
  import aiohttp
5
6
  from aiohttp import ClientSession
6
7
 
7
8
  from pymax.exceptions import Error
8
- from pymax.files import File, Photo
9
+ from pymax.files import File, Photo, Video
9
10
  from pymax.formatting import Formatting
10
11
  from pymax.interfaces import ClientProtocol
11
12
  from pymax.mixins.utils import MixinsUtils
@@ -27,6 +28,7 @@ from pymax.payloads import (
27
28
  SendMessagePayload,
28
29
  SendMessagePayloadMessage,
29
30
  UploadPayload,
31
+ VideoAttachPayload,
30
32
  )
31
33
  from pymax.static.constant import DEFAULT_TIMEOUT
32
34
  from pymax.static.enum import AttachType, Opcode
@@ -35,6 +37,7 @@ from pymax.types import (
35
37
  FileRequest,
36
38
  Message,
37
39
  ReactionInfo,
40
+ VideoAttach,
38
41
  VideoRequest,
39
42
  )
40
43
 
@@ -79,9 +82,8 @@ class MessageMixin(ClientProtocol):
79
82
  data=file_bytes,
80
83
  ) as response,
81
84
  ):
82
- if response.status != 200:
85
+ if response.status != HTTPStatus.OK:
83
86
  self.logger.error(f"Upload failed with status {response.status}")
84
- # cleanup waiter
85
87
  self._file_upload_waiters.pop(int(file_id), None)
86
88
  return None
87
89
 
@@ -97,7 +99,76 @@ class MessageMixin(ClientProtocol):
97
99
  return None
98
100
  except Exception as e:
99
101
  self.logger.exception("Upload file failed: %s", str(e))
100
- return None
102
+ raise e
103
+
104
+ async def _upload_video(self, video: Video) -> None | Attach:
105
+ try:
106
+ self.logger.info("Uploading video")
107
+ payload = UploadPayload().model_dump(by_alias=True)
108
+ data = await self._send_and_wait(
109
+ opcode=Opcode.VIDEO_UPLOAD,
110
+ payload=payload,
111
+ )
112
+
113
+ if data.get("payload", {}).get("error"):
114
+ MixinsUtils.handle_error(data)
115
+
116
+ url = data.get("payload", {}).get("info", [None])[0].get("url", None)
117
+ video_id = (
118
+ data.get("payload", {}).get("info", [None])[0].get("videoId", None)
119
+ )
120
+ if not url or not video_id:
121
+ self.logger.error("No upload URL or video ID received")
122
+ return None
123
+
124
+ token = data.get("payload", {}).get("info", [None])[0].get("token", None)
125
+ if not token:
126
+ self.logger.error("No upload token received")
127
+ return None
128
+
129
+ file_bytes = await video.read()
130
+
131
+ headers = {
132
+ "Content-Disposition": f"attachment; filename={video.file_name}",
133
+ "Content-Range": f"0-{len(file_bytes) - 1}/{len(file_bytes)}",
134
+ }
135
+
136
+ loop = asyncio.get_running_loop()
137
+ fut: asyncio.Future[dict] = loop.create_future()
138
+ try:
139
+ self._file_upload_waiters[int(video_id)] = fut
140
+ except Exception:
141
+ self.logger.exception("Failed to register file upload waiter")
142
+
143
+ async with (
144
+ ClientSession() as session,
145
+ session.post(
146
+ url=url,
147
+ headers=headers,
148
+ data=file_bytes,
149
+ ) as response,
150
+ ):
151
+ if response.status != HTTPStatus.OK:
152
+ self.logger.error(f"Upload failed with status {response.status}")
153
+ self._file_upload_waiters.pop(int(video_id), None)
154
+ return None
155
+
156
+ try:
157
+ await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
158
+ return Attach(
159
+ _type=AttachType.VIDEO, video_id=video_id, token=token
160
+ )
161
+ except asyncio.TimeoutError:
162
+ self.logger.error(
163
+ "Timed out waiting for video processing notification for videoId=%s",
164
+ video_id,
165
+ )
166
+ self._file_upload_waiters.pop(int(video_id), None)
167
+ return None
168
+
169
+ except Exception as e:
170
+ self.logger.exception("Upload video failed: %s", str(e))
171
+ raise e
101
172
 
102
173
  async def _upload_photo(self, photo: Photo) -> None | Attach:
103
174
  try:
@@ -137,7 +208,7 @@ class MessageMixin(ClientProtocol):
137
208
  data=form,
138
209
  ) as response,
139
210
  ):
140
- if response.status != 200:
211
+ if response.status != HTTPStatus.OK:
141
212
  self.logger.error(f"Upload failed with status {response.status}")
142
213
  return None
143
214
 
@@ -161,7 +232,7 @@ class MessageMixin(ClientProtocol):
161
232
  self.logger.exception("Upload photo failed: %s", str(e))
162
233
  return None
163
234
 
164
- async def _upload_attachment(self, attach: Photo | File) -> dict | None:
235
+ async def _upload_attachment(self, attach: Photo | File | Video) -> dict | None:
165
236
  if isinstance(attach, Photo):
166
237
  uploaded = await self._upload_photo(attach)
167
238
  if uploaded and uploaded.photo_token:
@@ -174,6 +245,12 @@ class MessageMixin(ClientProtocol):
174
245
  return AttachFilePayload(file_id=uploaded.file_id).model_dump(
175
246
  by_alias=True
176
247
  )
248
+ elif isinstance(attach, Video):
249
+ uploaded = await self._upload_video(attach)
250
+ if uploaded and uploaded.video_id and uploaded.token:
251
+ return VideoAttachPayload(
252
+ video_id=uploaded.video_id, token=uploaded.token
253
+ ).model_dump(by_alias=True)
177
254
  self.logger.error(f"Attachment upload failed for {attach}")
178
255
  return None
179
256
 
@@ -181,9 +258,9 @@ class MessageMixin(ClientProtocol):
181
258
  self,
182
259
  text: str,
183
260
  chat_id: int,
184
- notify: bool,
185
- attachment: Photo | File | None = None,
186
- attachments: list[Photo | File] | None = None,
261
+ notify: bool = True,
262
+ attachment: Photo | File | Video | None = None,
263
+ attachments: list[Photo | File | Video] | None = None,
187
264
  reply_to: int | None = None,
188
265
  use_queue: bool = False,
189
266
  ) -> Message | None:
@@ -268,8 +345,8 @@ class MessageMixin(ClientProtocol):
268
345
  chat_id: int,
269
346
  message_id: int,
270
347
  text: str,
271
- attachment: Photo | None = None,
272
- attachments: list[Photo] | None = None,
348
+ attachment: Photo | File | Video | None = None,
349
+ attachments: list[Photo | Video | File] | None = None,
273
350
  use_queue: bool = False,
274
351
  ) -> Message | None:
275
352
  self.logger.info(
@@ -480,7 +557,7 @@ class MessageMixin(ClientProtocol):
480
557
  video_id=video_id,
481
558
  ).model_dump(by_alias=True)
482
559
 
483
- data = await self._send_and_wait(opcode=Opcode.VIDEO_PLAY, payload=payload)
560
+ data = await self._send_and_wait(opcode=Opcode.VIDEO_PLAY, payload=payload)
484
561
 
485
562
  if data.get("payload", {}).get("error"):
486
563
  MixinsUtils.handle_error(data)
pymax/mixins/self.py CHANGED
@@ -1,8 +1,18 @@
1
+ from typing import Any
2
+ from uuid import uuid4
3
+
1
4
  from pymax.exceptions import Error
2
5
  from pymax.interfaces import ClientProtocol
3
6
  from pymax.mixins.utils import MixinsUtils
4
- from pymax.payloads import ChangeProfilePayload
7
+ from pymax.payloads import (
8
+ ChangeProfilePayload,
9
+ CreateFolderPayload,
10
+ DeleteFolderPayload,
11
+ GetFolderPayload,
12
+ UpdateFolderPayload,
13
+ )
5
14
  from pymax.static.enum import Opcode
15
+ from pymax.types import Folder, FolderList, FolderUpdate
6
16
 
7
17
 
8
18
  class SelfMixin(ClientProtocol):
@@ -39,3 +49,107 @@ class SelfMixin(ClientProtocol):
39
49
  MixinsUtils.handle_error(data)
40
50
 
41
51
  return True
52
+
53
+ async def create_folder(
54
+ self, title: str, chat_include: list[int], filters: list[Any] | None = None
55
+ ) -> FolderUpdate:
56
+ """
57
+ Создает папку для чатов
58
+
59
+ Args:
60
+ title (str): Название папки
61
+ chat_include (list[int]): Список ID чатов для включения в папку
62
+ filters (list[Any] | None, optional): Список фильтров для папки (Неизвестный параметр, использование на свой страх и риск)
63
+
64
+ Returns:
65
+ bool: True, если папка создана
66
+ """
67
+ self.logger.info("Creating folder")
68
+
69
+ payload = CreateFolderPayload(
70
+ id=str(uuid4()),
71
+ title=title,
72
+ include=chat_include,
73
+ filters=filters or [],
74
+ ).model_dump(by_alias=True)
75
+
76
+ data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
77
+
78
+ if data.get("payload", {}).get("error"):
79
+ MixinsUtils.handle_error(data)
80
+
81
+ return FolderUpdate.from_dict(data.get("payload", {}))
82
+
83
+ async def get_folders(self, folder_sync: int = 0) -> FolderList:
84
+ """
85
+ Получает список папок
86
+ Args:
87
+ folder_sync (int, optional): Синхронизационный маркер папок. По умолчанию 0. (Неизвестный параметр, использование на свой страх и риск)
88
+ Returns:
89
+ FolderList: Список папок
90
+ """
91
+ self.logger.info("Fetching folders")
92
+
93
+ payload = GetFolderPayload(folder_sync=folder_sync).model_dump(by_alias=True)
94
+
95
+ data = await self._send_and_wait(opcode=Opcode.FOLDERS_GET, payload=payload)
96
+
97
+ if data.get("payload", {}).get("error"):
98
+ MixinsUtils.handle_error(data)
99
+
100
+ return FolderList.from_dict(data.get("payload", {}))
101
+
102
+ async def update_folder(
103
+ self,
104
+ folder_id: str,
105
+ title: str,
106
+ chat_include: list[int] | None = None,
107
+ filters: list[Any] | None = None,
108
+ options: list[Any] | None = None,
109
+ ):
110
+ """
111
+ Обновляет папку для чатов
112
+
113
+ Args:
114
+ folder_id (str): ID папки
115
+ title (str): Название папки
116
+ chat_include (list[int] | None, optional): Список ID чатов для включения в папку. По умолчанию None.
117
+ filters (list[Any] | None, optional): Список фильтров для папки. По умолчанию None.
118
+ options (list[Any] | None, optional): Список опций для папки. По умолчанию None.
119
+ Returns:
120
+ """
121
+ self.logger.info("Updating folder")
122
+
123
+ payload = UpdateFolderPayload(
124
+ id=folder_id,
125
+ title=title,
126
+ include=chat_include or [],
127
+ filters=filters or [],
128
+ options=options or [],
129
+ ).model_dump(by_alias=True, exclude_none=True)
130
+
131
+ data = await self._send_and_wait(opcode=Opcode.FOLDERS_UPDATE, payload=payload)
132
+
133
+ if data.get("payload", {}).get("error"):
134
+ MixinsUtils.handle_error(data)
135
+
136
+ return FolderUpdate.from_dict(data.get("payload", {}))
137
+
138
+ async def delete_folder(self, folder_id: str) -> FolderUpdate:
139
+ """
140
+ Удаляет папку для чатов
141
+
142
+ Args:
143
+ folder_id (str): ID папки
144
+
145
+ Returns:
146
+ bool: True, если папка удалена
147
+ """
148
+ self.logger.info("Deleting folder")
149
+
150
+ payload = DeleteFolderPayload(folder_ids=[folder_id]).model_dump(by_alias=True)
151
+ data = await self._send_and_wait(opcode=Opcode.FOLDERS_DELETE, payload=payload)
152
+ if data.get("payload", {}).get("error"):
153
+ MixinsUtils.handle_error(data)
154
+
155
+ return FolderUpdate.from_dict(data.get("payload", {}))
pymax/mixins/socket.py CHANGED
@@ -92,37 +92,33 @@ class SocketMixin(ClientProtocol):
92
92
  payload_len_b = payload_len.to_bytes(4, "big")
93
93
  return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
94
94
 
95
- async def _connect(self, user_agent: UserAgentPayload) -> dict[str, Any]:
96
- try:
97
- if sys.version_info[:2] == (3, 12):
98
- self.logger.warning(
99
- """
95
+ async def connect(self, user_agent: UserAgentPayload) -> dict[str, Any]:
96
+ if sys.version_info[:2] == (3, 12):
97
+ self.logger.warning(
98
+ """
100
99
  ===============================================================
101
100
  ⚠️⚠️ \033[0;31mWARNING: Python 3.12 detected!\033[0m ⚠️⚠️
102
101
  Socket connections may be unstable, SSL issues are possible.
103
102
  ===============================================================
104
103
  """
105
- )
106
- self.logger.info("Connecting to socket %s:%s", self.host, self.port)
107
- loop = asyncio.get_running_loop()
108
- raw_sock = await loop.run_in_executor(
109
- None, lambda: socket.create_connection((self.host, self.port))
110
104
  )
111
- self._socket = self._ssl_context.wrap_socket(
112
- raw_sock, server_hostname=self.host
113
- )
114
- self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
115
- self.is_connected = True
116
- self._incoming = asyncio.Queue()
117
- self._outgoing = asyncio.Queue()
118
- self._pending = {}
119
- self._recv_task = asyncio.create_task(self._recv_loop())
120
- self._outgoing_task = asyncio.create_task(self._outgoing_loop())
121
- self.logger.info("Socket connected, starting handshake")
122
- return await self._handshake(user_agent)
123
- except Exception as e:
124
- self.logger.error("Failed to connect: %s", e, exc_info=True)
125
- raise ConnectionError(f"Failed to connect: {e}")
105
+ self.logger.info("Connecting to socket %s:%s", self.host, self.port)
106
+ loop = asyncio.get_running_loop()
107
+ raw_sock = await loop.run_in_executor(
108
+ None, lambda: socket.create_connection((self.host, self.port))
109
+ )
110
+ self._socket = self._ssl_context.wrap_socket(
111
+ raw_sock, server_hostname=self.host
112
+ )
113
+ self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
114
+ self.is_connected = True
115
+ self._incoming = asyncio.Queue()
116
+ self._outgoing = asyncio.Queue()
117
+ self._pending = {}
118
+ self._recv_task = asyncio.create_task(self._recv_loop())
119
+ self._outgoing_task = asyncio.create_task(self._outgoing_loop())
120
+ self.logger.info("Socket connected, starting handshake")
121
+ return await self._handshake(user_agent)
126
122
 
127
123
  async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
128
124
  try:
@@ -345,6 +341,35 @@ Socket connections may be unstable, SSL issues are possible.
345
341
  self.logger.exception(
346
342
  "Error in on_chat_update_handler: %s", e
347
343
  )
344
+
345
+ try: # TODO: переделать, временное решение
346
+ if data_item.get("opcode") == Opcode.NOTIF_ATTACH:
347
+ file_id = data_item.get("payload", {}).get(
348
+ "fileId", None
349
+ )
350
+ video_id = data_item.get("payload", {}).get(
351
+ "videoId", None
352
+ )
353
+ if file_id is not None:
354
+ fut = self._file_upload_waiters.pop(file_id, None)
355
+ if fut and not fut.done():
356
+ fut.set_result(data)
357
+ self.logger.debug(
358
+ "Fulfilled file upload waiter for fileId=%s",
359
+ file_id,
360
+ )
361
+ elif video_id is not None:
362
+ fut = self._file_upload_waiters.pop(video_id, None)
363
+ if fut and not fut.done():
364
+ fut.set_result(data)
365
+ self.logger.debug(
366
+ "Fulfilled file upload waiter for videoId=%s",
367
+ video_id,
368
+ )
369
+ except Exception:
370
+ self.logger.exception(
371
+ "Error handling file upload notification"
372
+ )
348
373
  except asyncio.CancelledError:
349
374
  self.logger.debug("Recv loop cancelled")
350
375
  break
@@ -396,6 +421,7 @@ Socket connections may be unstable, SSL issues are possible.
396
421
  ) -> dict[str, Any]:
397
422
  self._seq += 1
398
423
  msg = BaseWebSocketMessage(
424
+ ver=10,
399
425
  cmd=cmd,
400
426
  seq=self._seq,
401
427
  opcode=opcode.value,
@@ -416,6 +442,7 @@ Socket connections may be unstable, SSL issues are possible.
416
442
  ) -> dict[str, Any]:
417
443
  if not self.is_connected or self._socket is None:
418
444
  raise SocketNotConnectedError
445
+
419
446
  sock = self.sock
420
447
  msg = self._make_message(opcode, payload, cmd)
421
448
  loop = asyncio.get_running_loop()
@@ -448,7 +475,7 @@ Socket connections may be unstable, SSL issues are possible.
448
475
  self.logger.warning("Connection lost, reconnecting...")
449
476
  self.is_connected = False
450
477
  try:
451
- await self._connect(self.user_agent)
478
+ await self.connect(self.user_agent)
452
479
  except Exception as exc:
453
480
  self.logger.exception("Reconnect failed")
454
481
  raise exc from conn_err
pymax/mixins/websocket.py CHANGED
@@ -167,7 +167,8 @@ class WebSocketMixin(ClientProtocol):
167
167
  try: # TODO: переделать, временное решение
168
168
  if data.get("opcode") == Opcode.NOTIF_ATTACH:
169
169
  file_id = data.get("payload", {}).get("fileId", None)
170
- if isinstance(file_id, int):
170
+ video_id = data.get("payload", {}).get("videoId", None)
171
+ if file_id is not None:
171
172
  fut = self._file_upload_waiters.pop(file_id, None)
172
173
  if fut and not fut.done():
173
174
  fut.set_result(data)
@@ -175,6 +176,14 @@ class WebSocketMixin(ClientProtocol):
175
176
  "Fulfilled file upload waiter for fileId=%s",
176
177
  file_id,
177
178
  )
179
+ elif video_id is not None:
180
+ fut = self._file_upload_waiters.pop(video_id, None)
181
+ if fut and not fut.done():
182
+ fut.set_result(data)
183
+ self.logger.debug(
184
+ "Fulfilled file upload waiter for videoId=%s",
185
+ video_id,
186
+ )
178
187
  except Exception:
179
188
  self.logger.exception("Error handling file upload notification")
180
189
 
pymax/payloads.py CHANGED
@@ -73,6 +73,12 @@ class AttachPhotoPayload(CamelModel):
73
73
  photo_token: str
74
74
 
75
75
 
76
+ class VideoAttachPayload(CamelModel):
77
+ type: AttachType = Field(default=AttachType.VIDEO, alias="_type")
78
+ video_id: int
79
+ token: str
80
+
81
+
76
82
  class AttachFilePayload(CamelModel):
77
83
  type: AttachType = Field(default=AttachType.FILE, alias="_type")
78
84
  file_id: int
@@ -88,7 +94,7 @@ class SendMessagePayloadMessage(CamelModel):
88
94
  text: str
89
95
  cid: int
90
96
  elements: list[MessageElement]
91
- attaches: list[AttachPhotoPayload | AttachFilePayload]
97
+ attaches: list[AttachPhotoPayload | AttachFilePayload | VideoAttachPayload]
92
98
  link: ReplyLink | None = None
93
99
 
94
100
 
@@ -103,7 +109,7 @@ class EditMessagePayload(CamelModel):
103
109
  message_id: int
104
110
  text: str
105
111
  elements: list[MessageElement]
106
- attaches: list[AttachPhotoPayload]
112
+ attaches: list[AttachPhotoPayload | AttachFilePayload | VideoAttachPayload]
107
113
 
108
114
 
109
115
  class DeleteMessagePayload(CamelModel):
@@ -196,7 +202,7 @@ class ChangeGroupProfilePayload(CamelModel):
196
202
 
197
203
  class GetGroupMembersPayload(CamelModel):
198
204
  type: Literal["MEMBER"] = "MEMBER"
199
- marker: int
205
+ marker: int | None = None
200
206
  chat_id: int
201
207
  count: int
202
208
 
@@ -295,3 +301,30 @@ class RegisterPayload(CamelModel):
295
301
  first_name: str
296
302
  token: str
297
303
  token_type: AuthType = AuthType.REGISTER
304
+
305
+
306
+ class CreateFolderPayload(CamelModel):
307
+ id: str
308
+ title: str
309
+ include: list[int]
310
+ filters: list[Any] = []
311
+
312
+
313
+ class GetChatInfoPayload(CamelModel):
314
+ chat_ids: list[int]
315
+
316
+
317
+ class GetFolderPayload(CamelModel):
318
+ folder_sync: int = 0
319
+
320
+
321
+ class UpdateFolderPayload(CamelModel):
322
+ id: str
323
+ title: str
324
+ include: list[int]
325
+ filters: list[Any] = []
326
+ options: list[Any] = []
327
+
328
+
329
+ class DeleteFolderPayload(CamelModel):
330
+ folder_ids: list[str]
pymax/static/enum.py CHANGED
@@ -17,7 +17,6 @@ class Opcode(int, Enum):
17
17
  AUTH_CONFIRM = 23
18
18
  PRESET_AVATARS = 25
19
19
  ASSETS_GET = 26
20
- UNKNOWN_26 = 26
21
20
  ASSETS_UPDATE = 27
22
21
  ASSETS_GET_BY_IDS = 28
23
22
  ASSETS_ADD = 29
pymax/types.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import Any, Self
1
+ from typing import Any
2
2
 
3
- from typing_extensions import override
3
+ from typing_extensions import Self, override
4
4
 
5
5
  from .static.enum import (
6
6
  AccessType,
@@ -1052,3 +1052,115 @@ class Session:
1052
1052
  @override
1053
1053
  def __str__(self) -> str:
1054
1054
  return f"Session: {self.client} from {self.location} at {self.time} (current={self.current})"
1055
+
1056
+
1057
+ class Folder:
1058
+ def __init__(
1059
+ self,
1060
+ source_id: int,
1061
+ include: list[int],
1062
+ options: list[Any],
1063
+ update_time: int,
1064
+ id: str,
1065
+ filters: list[Any],
1066
+ title: str,
1067
+ ) -> None:
1068
+ self.source_id = source_id
1069
+ self.include = include
1070
+ self.options = options
1071
+ self.update_time = update_time
1072
+ self.id = id
1073
+ self.filters = filters
1074
+ self.title = title
1075
+
1076
+ @classmethod
1077
+ def from_dict(cls, data: dict[str, Any]) -> Self:
1078
+ return cls(
1079
+ source_id=data.get("sourceId", 0),
1080
+ include=data.get("include", []),
1081
+ options=data.get("options", []),
1082
+ update_time=data.get("updateTime", 0),
1083
+ id=data.get("id", ""),
1084
+ filters=data.get("filters", []),
1085
+ title=data.get("title", ""),
1086
+ )
1087
+
1088
+ @override
1089
+ def __repr__(self) -> str:
1090
+ return (
1091
+ f"Folder(id={self.id!r}, title={self.title!r}, source_id={self.source_id!r}, "
1092
+ f"include={self.include!r}, options={self.options!r}, "
1093
+ f"update_time={self.update_time!r}, filters={self.filters!r})"
1094
+ )
1095
+
1096
+ @override
1097
+ def __str__(self) -> str:
1098
+ return f"Folder: {self.title} ({self.id})"
1099
+
1100
+
1101
+ class FolderUpdate:
1102
+ def __init__(
1103
+ self, folder_order: list[str] | None, folder: Folder | None, folder_sync: int
1104
+ ) -> None:
1105
+ self.folder_order = folder_order
1106
+ self.folder = folder
1107
+ self.folder_sync = folder_sync
1108
+
1109
+ @classmethod
1110
+ def from_dict(cls, data: dict[str, Any]) -> Self:
1111
+ folder_order = data.get("foldersOrder", [])
1112
+ folder_sync = data.get("folderSync", 0)
1113
+ folder_data = data.get("folder", {})
1114
+ folder = Folder.from_dict(folder_data)
1115
+ return cls(
1116
+ folder_order=folder_order,
1117
+ folder=folder,
1118
+ folder_sync=folder_sync,
1119
+ )
1120
+
1121
+ @override
1122
+ def __repr__(self) -> str:
1123
+ return (
1124
+ f"FolderUpdate(folder_order={self.folder_order!r}, "
1125
+ f"folder={self.folder!r}, folder_sync={self.folder_sync!r})"
1126
+ )
1127
+
1128
+ @override
1129
+ def __str__(self) -> str:
1130
+ return f"FolderUpdate: {self.folder.title} ({self.folder.id})"
1131
+
1132
+
1133
+ class FolderList:
1134
+ def __init__(
1135
+ self,
1136
+ folders_order: list[str],
1137
+ folders: list[Folder],
1138
+ folder_sync: int,
1139
+ all_filter_exclude_folders: list[Any] | None = None,
1140
+ ) -> None:
1141
+ self.folders_order = folders_order
1142
+ self.folders = folders
1143
+ self.all_filter_exclude_folders = all_filter_exclude_folders or []
1144
+ self.folder_sync = folder_sync
1145
+
1146
+ @classmethod
1147
+ def from_dict(cls, data: dict[str, Any]) -> Self:
1148
+ return cls(
1149
+ folders_order=data.get("foldersOrder", []),
1150
+ folders=[Folder.from_dict(f) for f in data.get("folders", [])],
1151
+ all_filter_exclude_folders=data.get("allFilterExcludeFolders", []),
1152
+ folder_sync=data.get("folderSync", 0),
1153
+ )
1154
+
1155
+ @override
1156
+ def __repr__(self) -> str:
1157
+ return (
1158
+ f"FolderList(folders_order={self.folders_order!r}, "
1159
+ f"folders={self.folders!r}, "
1160
+ f"all_filter_exclude_folders={self.all_filter_exclude_folders!r}, "
1161
+ f"folder_sync={self.folder_sync!r})"
1162
+ )
1163
+
1164
+ @override
1165
+ def __str__(self) -> str:
1166
+ return f"FolderList: {len(self.folders)} folders"
@@ -1,31 +0,0 @@
1
- pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
- pymax/core.py,sha256=QEERtU32ODmEIf1XFCDhD18a29yRW5TB97NjtusMUCg,15644
3
- pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
4
- pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
- pymax/files.py,sha256=dRuOpvoJZWiH4xa_HVGyqQ-_Zzj-sVikElHmrPjwgs0,3166
6
- pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
7
- pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
8
- pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
9
- pymax/interfaces.py,sha256=OqYTiTUs6HqTkx3I3CU34q-En8nLS1Rx2hRlmEq65V4,3643
10
- pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
- pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
- pymax/payloads.py,sha256=S1dJwDPanFfIdY_NlXN2epVyibmmL9bceltgLVmEtTA,6304
13
- pymax/types.py,sha256=RaLn9bUpkxO0SKbDMIHnoFeqV6gqOl2pKDNCa2LxTRI,32102
14
- pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
15
- pymax/mixins/auth.py,sha256=zkMkALjvb2427g1DMcvUIKsOQgw1y8d-tEo3jlyNiWQ,6744
16
- pymax/mixins/channel.py,sha256=dMuJRnbqZisN8kcPFCCe1sIOOBQl2uT4P49PpZXcoKE,5206
17
- pymax/mixins/group.py,sha256=7oa7RpiqnlcnAsnIHOfSiujNYAzUZ9lkTy9NGW5KVUE,8654
18
- pymax/mixins/handler.py,sha256=TuO5bHK6qwJ-Wdh3lMg6uWaG6IwNPOUTCnMw2PkCFjA,5872
19
- pymax/mixins/message.py,sha256=ezU9d6r4MkYjH67gZ9SFLYPKqo4Nb6lswqDsEW5p-Bg,22329
20
- pymax/mixins/self.py,sha256=tDQrUdUpsCu7qGkWLtKxTfTHPHU5_r3qsn-eptHG2KY,1198
21
- pymax/mixins/socket.py,sha256=VsQSDyzP2xvQp-V8SgeOBn3vVQfWwi0m_wMIvlzu2lY,25517
22
- pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
23
- pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
24
- pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
25
- pymax/mixins/websocket.py,sha256=Rfn3PFfmey2u3e3xnebNeT9VoxBF9Dq20xM8ljaDiII,19286
26
- pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
27
- pymax/static/enum.py,sha256=c_QaLU0Ephe4SuKFIpwpmrf_HCutc34JJ6o4Ik1E6_g,4582
28
- maxapi_python-1.1.19.dist-info/METADATA,sha256=2tRU1Um8ZR6LHzm83ze-bWs0V5FI6ru6DNYHbi4JBw8,6245
29
- maxapi_python-1.1.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- maxapi_python-1.1.19.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
31
- maxapi_python-1.1.19.dist-info/RECORD,,