maxapi-python 1.2.2__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.2.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 +55 -37
- pymax/files.py +33 -7
- pymax/interfaces.py +410 -114
- pymax/mixins/auth.py +2 -2
- pymax/mixins/channel.py +3 -5
- pymax/mixins/group.py +33 -14
- pymax/mixins/handler.py +4 -10
- pymax/mixins/message.py +64 -88
- pymax/mixins/scheduler.py +1 -1
- pymax/mixins/self.py +76 -11
- pymax/mixins/socket.py +4 -327
- pymax/mixins/telemetry.py +2 -4
- pymax/mixins/user.py +3 -5
- pymax/mixins/websocket.py +5 -354
- pymax/payloads.py +11 -1
- pymax/protocols.py +123 -0
- pymax/static/constant.py +69 -8
- pymax/static/enum.py +6 -0
- pymax/types.py +82 -28
- pymax/utils.py +90 -0
- maxapi_python-1.2.2.dist-info/RECORD +0 -32
- pymax/mixins/utils.py +0 -27
- {maxapi_python-1.2.2.dist-info → maxapi_python-1.2.4.dist-info}/WHEEL +0 -0
- {maxapi_python-1.2.2.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,
|
|
@@ -116,10 +116,11 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
116
116
|
self.dialogs: list[Dialog] = []
|
|
117
117
|
self.channels: list[Channel] = []
|
|
118
118
|
self.me: Me | None = None
|
|
119
|
+
self.contacts: list[User] = []
|
|
119
120
|
self._users: dict[int, User] = {}
|
|
120
121
|
|
|
121
122
|
self._work_dir: str = work_dir
|
|
122
|
-
self._database_path: Path = Path(work_dir) /
|
|
123
|
+
self._database_path: Path = Path(work_dir) / session_name
|
|
123
124
|
self._database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
125
|
self._database_path.touch(exist_ok=True)
|
|
125
126
|
self._database = Database(self._work_dir)
|
|
@@ -131,6 +132,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
131
132
|
self._pending: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
132
133
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
133
134
|
self._background_tasks: set[asyncio.Task[Any]] = set()
|
|
135
|
+
self._stop_event = asyncio.Event()
|
|
134
136
|
|
|
135
137
|
self._seq: int = 0
|
|
136
138
|
self._error_count: int = 0
|
|
@@ -141,7 +143,10 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
141
143
|
self._file_upload_waiters: dict[int, asyncio.Future[dict[str, Any]]] = {}
|
|
142
144
|
|
|
143
145
|
self._token = self._database.get_auth_token() or token
|
|
146
|
+
if headers is None:
|
|
147
|
+
headers = self._default_headers()
|
|
144
148
|
self.user_agent = headers
|
|
149
|
+
self._validate_device_type()
|
|
145
150
|
self._send_fake_telemetry: bool = send_fake_telemetry
|
|
146
151
|
self._session_id: int = int(time.time() * 1000)
|
|
147
152
|
self._action_id: int = 1
|
|
@@ -179,11 +184,24 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
179
184
|
self._work_dir,
|
|
180
185
|
)
|
|
181
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
|
+
|
|
182
198
|
async def _wait_forever(self) -> None:
|
|
183
199
|
try:
|
|
184
200
|
await self.ws.wait_closed()
|
|
185
201
|
except asyncio.CancelledError:
|
|
186
202
|
self.logger.debug("wait_closed cancelled")
|
|
203
|
+
except WebSocketNotConnectedError:
|
|
204
|
+
self.logger.info("WebSocket not connected, exiting wait_forever")
|
|
187
205
|
|
|
188
206
|
async def close(self) -> None:
|
|
189
207
|
"""
|
|
@@ -193,22 +211,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
193
211
|
"""
|
|
194
212
|
try:
|
|
195
213
|
self.logger.info("Closing client")
|
|
196
|
-
|
|
197
|
-
self._recv_task.cancel()
|
|
198
|
-
try:
|
|
199
|
-
await self._recv_task
|
|
200
|
-
except asyncio.CancelledError:
|
|
201
|
-
self.logger.debug("recv_task cancelled")
|
|
202
|
-
if self._outgoing_task:
|
|
203
|
-
self._outgoing_task.cancel()
|
|
204
|
-
try:
|
|
205
|
-
await self._outgoing_task
|
|
206
|
-
except asyncio.CancelledError:
|
|
207
|
-
self.logger.debug("outgoing_task cancelled")
|
|
208
|
-
if self._ws:
|
|
209
|
-
await self._ws.close()
|
|
210
|
-
self.is_connected = False
|
|
211
|
-
self.logger.info("Client closed")
|
|
214
|
+
self._stop_event.set()
|
|
212
215
|
except Exception:
|
|
213
216
|
self.logger.exception("Error closing client")
|
|
214
217
|
|
|
@@ -278,10 +281,9 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
278
281
|
:return: None
|
|
279
282
|
:rtype: None
|
|
280
283
|
"""
|
|
281
|
-
|
|
282
|
-
while
|
|
284
|
+
self.logger.info("Client starting")
|
|
285
|
+
while not self._stop_event.is_set():
|
|
283
286
|
try:
|
|
284
|
-
self.logger.info("Client starting")
|
|
285
287
|
await self.connect(self.user_agent)
|
|
286
288
|
|
|
287
289
|
if self.registration:
|
|
@@ -296,29 +298,45 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
|
|
|
296
298
|
await self._login()
|
|
297
299
|
|
|
298
300
|
await self._sync(self.user_agent)
|
|
299
|
-
|
|
300
301
|
await self._post_login_tasks(sync=False)
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
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
|
|
304
318
|
except Exception as e:
|
|
305
319
|
self.logger.exception("Client start iteration failed")
|
|
306
|
-
raise e
|
|
307
|
-
|
|
308
320
|
finally:
|
|
309
|
-
self.logger.debug("Cleaning up background tasks and pending futures")
|
|
310
|
-
|
|
311
321
|
await self._cleanup_client()
|
|
312
322
|
|
|
313
|
-
if not self.reconnect:
|
|
314
|
-
self.logger.info("Reconnect disabled — exiting start()")
|
|
315
|
-
|
|
323
|
+
if not self.reconnect or self._stop_event.is_set():
|
|
324
|
+
self.logger.info("Reconnect disabled or stop requested — exiting start()")
|
|
325
|
+
break
|
|
316
326
|
|
|
317
327
|
self.logger.info("Reconnect enabled — restarting client")
|
|
318
328
|
await asyncio.sleep(self.reconnect_delay)
|
|
319
329
|
|
|
330
|
+
self.logger.info("Client exited cleanly")
|
|
331
|
+
|
|
320
332
|
|
|
321
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
|
+
|
|
322
340
|
@override
|
|
323
341
|
async def _wait_forever(self):
|
|
324
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,8 +51,24 @@ class Photo(BaseFile):
|
|
|
45
51
|
".bmp",
|
|
46
52
|
} # FIXME: костыль ✅
|
|
47
53
|
|
|
48
|
-
def __init__(
|
|
49
|
-
|
|
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:
|
|
62
|
+
if path:
|
|
63
|
+
self.file_name = Path(path).name
|
|
64
|
+
elif url:
|
|
65
|
+
self.file_name = Path(url).name
|
|
66
|
+
elif name:
|
|
67
|
+
self.file_name = name
|
|
68
|
+
else:
|
|
69
|
+
self.file_name = ""
|
|
70
|
+
|
|
71
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
50
72
|
|
|
51
73
|
def validate_photo(self) -> tuple[str, str] | None:
|
|
52
74
|
if self.path:
|
|
@@ -78,7 +100,9 @@ class Photo(BaseFile):
|
|
|
78
100
|
|
|
79
101
|
|
|
80
102
|
class Video(BaseFile):
|
|
81
|
-
def __init__(
|
|
103
|
+
def __init__(
|
|
104
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
105
|
+
) -> None:
|
|
82
106
|
self.file_name: str = ""
|
|
83
107
|
if path:
|
|
84
108
|
self.file_name = Path(path).name
|
|
@@ -87,7 +111,7 @@ class Video(BaseFile):
|
|
|
87
111
|
|
|
88
112
|
if not self.file_name:
|
|
89
113
|
raise ValueError("Either url or path must be provided.")
|
|
90
|
-
super().__init__(url, path)
|
|
114
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
91
115
|
|
|
92
116
|
@override
|
|
93
117
|
async def read(self) -> bytes:
|
|
@@ -95,7 +119,9 @@ class Video(BaseFile):
|
|
|
95
119
|
|
|
96
120
|
|
|
97
121
|
class File(BaseFile):
|
|
98
|
-
def __init__(
|
|
122
|
+
def __init__(
|
|
123
|
+
self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
|
|
124
|
+
) -> None:
|
|
99
125
|
self.file_name: str = ""
|
|
100
126
|
if path:
|
|
101
127
|
self.file_name = Path(path).name
|
|
@@ -105,7 +131,7 @@ class File(BaseFile):
|
|
|
105
131
|
if not self.file_name:
|
|
106
132
|
raise ValueError("Either url or path must be provided.")
|
|
107
133
|
|
|
108
|
-
super().__init__(url, path)
|
|
134
|
+
super().__init__(raw=raw, url=url, path=path)
|
|
109
135
|
|
|
110
136
|
@override
|
|
111
137
|
async def read(self) -> bytes:
|