maxapi-python 1.2.3__py3-none-any.whl → 1.2.4__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.
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.4.dist-info}/METADATA +12 -7
- maxapi_python-1.2.4.dist-info/RECORD +33 -0
- pymax/core.py +54 -37
- pymax/files.py +28 -7
- pymax/interfaces.py +410 -115
- pymax/mixins/auth.py +2 -2
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +2 -2
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +2 -2
- pymax/mixins/socket.py +3 -336
- pymax/mixins/telemetry.py +2 -4
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +5 -363
- pymax/payloads.py +8 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +69 -8
- pymax/static/enum.py +5 -0
- pymax/types.py +25 -0
- pymax/utils.py +90 -0
- maxapi_python-1.2.3.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.4.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.3.dist-info → maxapi_python-1.2.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxapi-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Python wrapper для API мессенджера Max
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Issues, https://github.com/
|
|
5
|
+
Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
|
|
6
|
+
Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
|
|
7
|
+
Project-URL: Issues, https://github.com/MaxApiTeam/PyMax/issues
|
|
8
8
|
Author-email: ink <mail@gmail.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -18,6 +18,7 @@ Requires-Dist: lz4>=4.4.4
|
|
|
18
18
|
Requires-Dist: msgpack>=1.1.1
|
|
19
19
|
Requires-Dist: qrcode>=8.2
|
|
20
20
|
Requires-Dist: sqlmodel>=0.0.24
|
|
21
|
+
Requires-Dist: ua-generator>=2.0.19
|
|
21
22
|
Requires-Dist: websockets>=15.0
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: flake8; extra == 'test'
|
|
@@ -36,6 +37,10 @@ Description-Content-Type: text/markdown
|
|
|
36
37
|
<strong>Python wrapper для API мессенджера Max</strong>
|
|
37
38
|
</p>
|
|
38
39
|
|
|
40
|
+
> [!IMPORTANT]
|
|
41
|
+
> (29.12.2025) Снова неожиданное изменение апи, теперь `MaxClient` с `device_type` любым кроме `WEB` не работает, для вохда по номеру телефона используйте `SocketMaxClient`
|
|
42
|
+
|
|
43
|
+
|
|
39
44
|
<p align="center">
|
|
40
45
|
<img src="https://img.shields.io/badge/python-3.10+-3776AB.svg" alt="Python 3.11+">
|
|
41
46
|
<img src="https://img.shields.io/badge/License-MIT-2f9872.svg" alt="License: MIT">
|
|
@@ -92,12 +97,12 @@ uv add -U maxapi-python
|
|
|
92
97
|
**Вход по номеру телефона (DESKTOP):**
|
|
93
98
|
|
|
94
99
|
```python
|
|
95
|
-
from pymax import
|
|
100
|
+
from pymax import SocketMaxClient
|
|
96
101
|
from pymax.payloads import UserAgentPayload
|
|
97
102
|
|
|
98
103
|
ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
|
|
99
104
|
|
|
100
|
-
client =
|
|
105
|
+
client = SocketMaxClient(
|
|
101
106
|
phone="+79111111111",
|
|
102
107
|
work_dir="cache",
|
|
103
108
|
headers=ua,
|
|
@@ -195,6 +200,6 @@ if __name__ == "__main__":
|
|
|
195
200
|
|
|
196
201
|
Спасибо всем за помощь в разработке!
|
|
197
202
|
|
|
198
|
-
<a href="https://github.com/
|
|
203
|
+
<a href="https://github.com/MaxApiTeam/PyMax/graphs/contributors">
|
|
199
204
|
<img src="https://contrib.rocks/image?repo=ink-developer/PyMax" />
|
|
200
205
|
</a>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
+
pymax/core.py,sha256=oJEgLVWHjtTPqDd8I7mdHgYxw-Z567FZx-su-iIhVK8,15904
|
|
3
|
+
pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
|
|
4
|
+
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
+
pymax/files.py,sha256=nx7oZfIJ8ZvO-TuG5LzSmk8esbBtNrkKdFQgTQVbUA8,4063
|
|
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=ZFmgZ9sK5j3jG8z7DuVovBkXIvPrN4H4rI0UBb2g4BY,19543
|
|
10
|
+
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
+
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
+
pymax/payloads.py,sha256=m0Fn0eEOo6hrdLB6ACh27ioT3SkYIMDMfLKaPbs2vdo,8147
|
|
13
|
+
pymax/protocols.py,sha256=PoNvri9jFH6WBXGwugrkU6lwtwJEw0DO2s13HOH8_KI,4025
|
|
14
|
+
pymax/types.py,sha256=z1HXNl8CP_X3jTUlENlF9_vzZKdb7gF5PHG5d4rG3BY,37209
|
|
15
|
+
pymax/utils.py,sha256=HK6E6UYyjtUoJ2KXWeDycyiXm_9j5shZme6VFA2ixeM,2960
|
|
16
|
+
pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
|
|
17
|
+
pymax/mixins/auth.py,sha256=nA1fX2ERzk6_WBSrbtBhvwkA5aUXgUdgweytqhZSAJU,14708
|
|
18
|
+
pymax/mixins/channel.py,sha256=Qi5ujw5X7QYx4Lq1XnvlJ3BYjmTFmYDfu7_jRcx4Mx8,5335
|
|
19
|
+
pymax/mixins/group.py,sha256=6bsWSx0ULZonDM_dJSC0EkqpZWd6tv9lmmKWj_gEaaw,15903
|
|
20
|
+
pymax/mixins/handler.py,sha256=duL3Q5Bvv8tjUhOKaDr_k7w09BeCwjVdws2ga7v_zNE,12432
|
|
21
|
+
pymax/mixins/message.py,sha256=MpUED92iWONkJRjb0f7lwPj9gYJhDuv0KKzbveOEaAk,33397
|
|
22
|
+
pymax/mixins/scheduler.py,sha256=K4HB9IfksnXPujJnkipIS5um9nuzC8EjbtQn65RtbfI,963
|
|
23
|
+
pymax/mixins/self.py,sha256=Wn9l3zDF5VzFzzisryOQknf3Ngl81Q98_9jqqbE9ZAw,9174
|
|
24
|
+
pymax/mixins/socket.py,sha256=00uO4-8xer94i6Z6NDTUzzEQbs3NVMH1DB-KoPY6shU,10525
|
|
25
|
+
pymax/mixins/telemetry.py,sha256=EAbGyk8EB6QxijaFQ16vUmFPe6l-gEraCxXnAfhA3kY,3594
|
|
26
|
+
pymax/mixins/user.py,sha256=Xwb2fWM8RCq0SbVhlRyr1RBQGyjlaImtp0lT2PbgEqE,9420
|
|
27
|
+
pymax/mixins/websocket.py,sha256=XZ7lE8rKiNd3MXQRlS7Waz8TuoVWms9ldjCOF12pNTw,4891
|
|
28
|
+
pymax/static/constant.py,sha256=Gu1j4ibpaZL3tI6fUayS_jkyYOYNe9K-QlTUCOGviwQ,2260
|
|
29
|
+
pymax/static/enum.py,sha256=Scyi1pAUaaQlec1YQsU_nvlfxTiQZ6p7gntOJhWfBk4,4773
|
|
30
|
+
maxapi_python-1.2.4.dist-info/METADATA,sha256=hVzJYUCzdYWHmbcvAVlNYrEmvLRoWSVxwSbeNCL0Rqc,7069
|
|
31
|
+
maxapi_python-1.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
maxapi_python-1.2.4.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
33
|
+
maxapi_python-1.2.4.dist-info/RECORD,,
|
pymax/core.py
CHANGED
|
@@ -17,15 +17,12 @@ from .crud import Database
|
|
|
17
17
|
from .exceptions import (
|
|
18
18
|
InvalidPhoneError,
|
|
19
19
|
SocketNotConnectedError,
|
|
20
|
+
WebSocketNotConnectedError,
|
|
20
21
|
)
|
|
21
22
|
from .interfaces import BaseClient
|
|
22
23
|
from .mixins import ApiMixin, SocketMixin, WebSocketMixin
|
|
23
24
|
from .payloads import UserAgentPayload
|
|
24
|
-
from .static.constant import
|
|
25
|
-
HOST,
|
|
26
|
-
PORT,
|
|
27
|
-
WEBSOCKET_URI,
|
|
28
|
-
)
|
|
25
|
+
from .static.constant import HOST, PORT, SESSION_STORAGE_DB, WEBSOCKET_URI
|
|
29
26
|
|
|
30
27
|
if TYPE_CHECKING:
|
|
31
28
|
from collections.abc import Callable
|
|
@@ -34,7 +31,6 @@ if TYPE_CHECKING:
|
|
|
34
31
|
|
|
35
32
|
from pymax.filters import BaseFilter
|
|
36
33
|
|
|
37
|
-
from .filters import Filters
|
|
38
34
|
from .types import Channel, Chat, Dialog, Me, Message, ReactionInfo, User
|
|
39
35
|
|
|
40
36
|
|
|
@@ -42,6 +38,7 @@ logger = logging.getLogger(__name__)
|
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
41
|
+
allowed_device_types: set[str] = {"WEB"}
|
|
45
42
|
"""
|
|
46
43
|
Основной клиент для работы с WebSocket API сервиса Max.
|
|
47
44
|
|
|
@@ -49,6 +46,8 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
49
46
|
:type phone: str
|
|
50
47
|
:param uri: URI WebSocket сервера.
|
|
51
48
|
:type uri: str, optional
|
|
49
|
+
:param session_name: Название сессии для хранения базы данных.
|
|
50
|
+
:type session_name: str, optional
|
|
52
51
|
:param work_dir: Рабочая директория для хранения базы данных.
|
|
53
52
|
:type work_dir: str, optional
|
|
54
53
|
:param logger: Пользовательский логгер. Если не передан, используется логгер модуля с именем f"{__name__}.MaxClient".
|
|
@@ -81,7 +80,8 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
81
80
|
self,
|
|
82
81
|
phone: str,
|
|
83
82
|
uri: str = WEBSOCKET_URI,
|
|
84
|
-
|
|
83
|
+
session_name: str = SESSION_STORAGE_DB,
|
|
84
|
+
headers: UserAgentPayload | None = None,
|
|
85
85
|
token: str | None = None,
|
|
86
86
|
send_fake_telemetry: bool = True,
|
|
87
87
|
host: str = HOST,
|
|
@@ -120,7 +120,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
120
120
|
self._users: dict[int, User] = {}
|
|
121
121
|
|
|
122
122
|
self._work_dir: str = work_dir
|
|
123
|
-
self._database_path: Path = Path(work_dir) /
|
|
123
|
+
self._database_path: Path = Path(work_dir) / session_name
|
|
124
124
|
self._database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
125
|
self._database_path.touch(exist_ok=True)
|
|
126
126
|
self._database = Database(self._work_dir)
|
|
@@ -132,6 +132,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
132
132
|
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
133
133
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
134
134
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
135
|
+
self._stop_event = asyncio.Event()
|
|
135
136
|
|
|
136
137
|
self._seq: int = 0
|
|
137
138
|
self._error_count: int = 0
|
|
@@ -142,7 +143,10 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
142
143
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
143
144
|
|
|
144
145
|
self._token = self._database.get_auth_token() or token
|
|
146
|
+
if headers is None:
|
|
147
|
+
headers = self._default_headers()
|
|
145
148
|
self.user_agent = headers
|
|
149
|
+
self._validate_device_type()
|
|
146
150
|
self._send_fake_telemetry: bool = send_fake_telemetry
|
|
147
151
|
self._session_id: int = int(time.time() * 1000)
|
|
148
152
|
self._action_id: int = 1
|
|
@@ -180,11 +184,24 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
180
184
|
self._work_dir,
|
|
181
185
|
)
|
|
182
186
|
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _default_headers() -> UserAgentPayload:
|
|
189
|
+
return UserAgentPayload(device_type="WEB")
|
|
190
|
+
|
|
191
|
+
def _validate_device_type(self) -> None:
|
|
192
|
+
if self.user_agent.device_type not in self.allowed_device_types:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"{self.__class__.__name__} does not support "
|
|
195
|
+
f"device_type={self.user_agent.device_type}"
|
|
196
|
+
)
|
|
197
|
+
|
|
183
198
|
async def _wait_forever(self) -> None:
|
|
184
199
|
try:
|
|
185
200
|
await self.ws.wait_closed()
|
|
186
201
|
except asyncio.CancelledError:
|
|
187
202
|
self.logger.debug("wait_closed cancelled")
|
|
203
|
+
except WebSocketNotConnectedError:
|
|
204
|
+
self.logger.info("WebSocket not connected, exiting wait_forever")
|
|
188
205
|
|
|
189
206
|
async def close(self) -> None:
|
|
190
207
|
"""
|
|
@@ -194,22 +211,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
194
211
|
"""
|
|
195
212
|
try:
|
|
196
213
|
self.logger.info("Closing client")
|
|
197
|
-
|
|
198
|
-
self._recv_task.cancel()
|
|
199
|
-
try:
|
|
200
|
-
await self._recv_task
|
|
201
|
-
except asyncio.CancelledError:
|
|
202
|
-
self.logger.debug("recv_task cancelled")
|
|
203
|
-
if self._outgoing_task:
|
|
204
|
-
self._outgoing_task.cancel()
|
|
205
|
-
try:
|
|
206
|
-
await self._outgoing_task
|
|
207
|
-
except asyncio.CancelledError:
|
|
208
|
-
self.logger.debug("outgoing_task cancelled")
|
|
209
|
-
if self._ws:
|
|
210
|
-
await self._ws.close()
|
|
211
|
-
self.is_connected = False
|
|
212
|
-
self.logger.info("Client closed")
|
|
214
|
+
self._stop_event.set()
|
|
213
215
|
except Exception:
|
|
214
216
|
self.logger.exception("Error closing client")
|
|
215
217
|
|
|
@@ -279,10 +281,9 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
279
281
|
:return: None
|
|
280
282
|
:rtype: None
|
|
281
283
|
"""
|
|
282
|
-
|
|
283
|
-
while
|
|
284
|
+
self.logger.info("Client starting")
|
|
285
|
+
while not self._stop_event.is_set():
|
|
284
286
|
try:
|
|
285
|
-
self.logger.info("Client starting")
|
|
286
287
|
await self.connect(self.user_agent)
|
|
287
288
|
|
|
288
289
|
if self.registration:
|
|
@@ -297,29 +298,45 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
297
298
|
await self._login()
|
|
298
299
|
|
|
299
300
|
await self._sync(self.user_agent)
|
|
300
|
-
|
|
301
301
|
await self._post_login_tasks(sync=False)
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
self.
|
|
303
|
+
wait_task = asyncio.create_task(self._wait_forever())
|
|
304
|
+
stop_task = asyncio.create_task(self._stop_event.wait())
|
|
305
|
+
|
|
306
|
+
done, pending = await asyncio.wait(
|
|
307
|
+
[wait_task, stop_task], return_when=asyncio.FIRST_COMPLETED
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
for task in pending:
|
|
311
|
+
task.cancel()
|
|
312
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
313
|
+
await task
|
|
314
|
+
|
|
315
|
+
except asyncio.CancelledError:
|
|
316
|
+
self.logger.info("Client task cancelled, stopping")
|
|
317
|
+
break
|
|
305
318
|
except Exception as e:
|
|
306
319
|
self.logger.exception("Client start iteration failed")
|
|
307
|
-
raise e
|
|
308
|
-
|
|
309
320
|
finally:
|
|
310
|
-
self.logger.debug("Cleaning up background tasks and pending futures")
|
|
311
|
-
|
|
312
321
|
await self._cleanup_client()
|
|
313
322
|
|
|
314
|
-
if not self.reconnect:
|
|
315
|
-
self.logger.info("Reconnect disabled — exiting start()")
|
|
316
|
-
|
|
323
|
+
if not self.reconnect or self._stop_event.is_set():
|
|
324
|
+
self.logger.info("Reconnect disabled or stop requested — exiting start()")
|
|
325
|
+
break
|
|
317
326
|
|
|
318
327
|
self.logger.info("Reconnect enabled — restarting client")
|
|
319
328
|
await asyncio.sleep(self.reconnect_delay)
|
|
320
329
|
|
|
330
|
+
self.logger.info("Client exited cleanly")
|
|
331
|
+
|
|
321
332
|
|
|
322
333
|
class SocketMaxClient(SocketMixin, MaxClient):
|
|
334
|
+
allowed_device_types = {"ANDROID", "IOS", "DESKTOP"}
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _default_headers() -> UserAgentPayload:
|
|
338
|
+
return UserAgentPayload(device_type="DESKTOP")
|
|
339
|
+
|
|
323
340
|
@override
|
|
324
341
|
async def _wait_forever(self):
|
|
325
342
|
if self._recv_task:
|
pymax/files.py
CHANGED
|
@@ -9,7 +9,10 @@ from typing_extensions import override
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BaseFile(ABC):
|
|
12
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
14
|
+
) -> None:
|
|
15
|
+
self.raw = raw
|
|
13
16
|
self.url = url
|
|
14
17
|
self.path = path
|
|
15
18
|
|
|
@@ -21,6 +24,9 @@ class BaseFile(ABC):
|
|
|
21
24
|
|
|
22
25
|
@abstractmethod
|
|
23
26
|
async def read(self) -> bytes:
|
|
27
|
+
if self.raw is not None:
|
|
28
|
+
return self.raw
|
|
29
|
+
|
|
24
30
|
if self.url:
|
|
25
31
|
async with (
|
|
26
32
|
ClientSession() as session,
|
|
@@ -45,13 +51,24 @@ class Photo(BaseFile):
|
|
|
45
51
|
".bmp",
|
|
46
52
|
} # FIXME: костыль ✅
|
|
47
53
|
|
|
48
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
raw: bytes | None = None,
|
|
57
|
+
*,
|
|
58
|
+
url: str | None = None,
|
|
59
|
+
path: str | None = None,
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
) -> None:
|
|
49
62
|
if path:
|
|
50
63
|
self.file_name = Path(path).name
|
|
51
64
|
elif url:
|
|
52
65
|
self.file_name = Path(url).name
|
|
66
|
+
elif name:
|
|
67
|
+
self.file_name = name
|
|
68
|
+
else:
|
|
69
|
+
self.file_name = ""
|
|
53
70
|
|
|
54
|
-
super().__init__(url, path)
|
|
71
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
55
72
|
|
|
56
73
|
def validate_photo(self) -> tuple[str, str] | None:
|
|
57
74
|
if self.path:
|
|
@@ -83,7 +100,9 @@ class Photo(BaseFile):
|
|
|
83
100
|
|
|
84
101
|
|
|
85
102
|
class Video(BaseFile):
|
|
86
|
-
def __init__(
|
|
103
|
+
def __init__(
|
|
104
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
105
|
+
) -> None:
|
|
87
106
|
self.file_name: str = ""
|
|
88
107
|
if path:
|
|
89
108
|
self.file_name = Path(path).name
|
|
@@ -92,7 +111,7 @@ class Video(BaseFile):
|
|
|
92
111
|
|
|
93
112
|
if not self.file_name:
|
|
94
113
|
raise ValueError("Either url or path must be provided.")
|
|
95
|
-
super().__init__(url, path)
|
|
114
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
96
115
|
|
|
97
116
|
@override
|
|
98
117
|
async def read(self) -> bytes:
|
|
@@ -100,7 +119,9 @@ class Video(BaseFile):
|
|
|
100
119
|
|
|
101
120
|
|
|
102
121
|
class File(BaseFile):
|
|
103
|
-
def __init__(
|
|
122
|
+
def __init__(
|
|
123
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
124
|
+
) -> None:
|
|
104
125
|
self.file_name: str = ""
|
|
105
126
|
if path:
|
|
106
127
|
self.file_name = Path(path).name
|
|
@@ -110,7 +131,7 @@ class File(BaseFile):
|
|
|
110
131
|
if not self.file_name:
|
|
111
132
|
raise ValueError("Either url or path must be provided.")
|
|
112
133
|
|
|
113
|
-
super().__init__(url, path)
|
|
134
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
114
135
|
|
|
115
136
|
@override
|
|
116
137
|
async def read(self) -> bytes:
|