async-mega-py 2.1.1__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/PKG-INFO +1 -1
  2. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/pyproject.toml +1 -1
  3. async_mega_py-2.2.1/src/mega/__init__.py +8 -0
  4. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/__main__.py +6 -2
  5. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/api.py +76 -50
  6. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/client.py +7 -4
  7. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/data_structures.py +3 -0
  8. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/transfer_it.py +6 -2
  9. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/utils.py +2 -1
  10. async_mega_py-2.1.1/src/mega/__init__.py +0 -4
  11. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/LICENSE +0 -0
  12. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/README.md +0 -0
  13. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/auth.py +0 -0
  14. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/chunker.py +0 -0
  15. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/core.py +0 -0
  16. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/crypto.py +0 -0
  17. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/download.py +0 -0
  18. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/env.py +0 -0
  19. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/errors.py +0 -0
  20. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/filesystem.py +0 -0
  21. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/progress/__init__.py +0 -0
  22. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/progress/rich_progress.py +0 -0
  23. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/py.typed +0 -0
  24. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/upload.py +0 -0
  25. {async_mega_py-2.1.1 → async_mega_py-2.2.1}/src/mega/vault.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-mega.py
3
- Version: 2.1.1
3
+ Version: 2.2.1
4
4
  Summary: Python library for the Mega.nz and Transfer.it API
5
5
  Keywords: api,downloader,mega,mega.nz,transfer.it
6
6
  Author: NTFSvolume
@@ -34,7 +34,7 @@ license = "Apache-2.0"
34
34
  license-files = ["LICENSE"]
35
35
  readme = "README.md"
36
36
  requires-python = ">=3.11"
37
- version = "2.1.1"
37
+ version = "2.2.1"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  cli = [
@@ -0,0 +1,8 @@
1
+ import importlib.metadata
2
+ from contextvars import ContextVar
3
+
4
+ _package_name_ = "async-mega-py"
5
+ __version__ = importlib.metadata.version(_package_name_)
6
+
7
+ LOG_FILE_PROGRESS: ContextVar[bool] = ContextVar("LOG_FILE_PROGRESS", default=True)
8
+ LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False)
@@ -1,3 +1,4 @@
1
+ # ruff: noqa: E402
1
2
  from __future__ import annotations
2
3
 
3
4
  import contextlib
@@ -6,11 +7,14 @@ import logging
6
7
  from pathlib import Path
7
8
  from typing import TYPE_CHECKING, Annotated
8
9
 
10
+ from rich.traceback import install as install_rich_traceback
11
+
12
+ _ = install_rich_traceback(width=None, max_frames=2)
13
+
9
14
  import yarl
10
15
  from cyclopts import App, Parameter
11
16
 
12
- from mega import __version__, env
13
- from mega.api import LOG_HTTP_TRAFFIC
17
+ from mega import LOG_HTTP_TRAFFIC, __version__, env
14
18
  from mega.client import MegaNzClient
15
19
  from mega.transfer_it import TransferItClient
16
20
  from mega.utils import Site, setup_logger
@@ -3,17 +3,18 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import contextlib
5
5
  import dataclasses
6
+ import json
6
7
  import logging
8
+ import uuid
7
9
  from collections.abc import Mapping, Sequence
8
- from contextvars import ContextVar
9
10
  from functools import wraps
10
- from types import MappingProxyType
11
- from typing import TYPE_CHECKING, Any, ClassVar, Literal, ParamSpec, Self, TypeVar
11
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, Self, TypeVar
12
12
 
13
13
  import aiohttp
14
14
  import yarl
15
15
  from aiolimiter import AsyncLimiter
16
16
 
17
+ from mega import LOG_HTTP_TRAFFIC, __version__, _package_name_
17
18
  from mega.crypto import generate_hashcash
18
19
  from mega.errors import RequestError, RetryRequestError
19
20
  from mega.utils import random_id, random_u32int
@@ -25,11 +26,6 @@ if TYPE_CHECKING:
25
26
  _R = TypeVar("_R")
26
27
 
27
28
 
28
- _UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0"
29
- _DEFAULT_HEADERS: MappingProxyType[str, str] = MappingProxyType({"User-Agent": _UA})
30
-
31
-
32
- LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False)
33
29
  logger = logging.getLogger(__name__)
34
30
 
35
31
 
@@ -67,25 +63,23 @@ def retry(
67
63
  return wrapper
68
64
 
69
65
 
70
- @dataclasses.dataclass(slots=True, weakref_slot=True, init=False)
66
+ @dataclasses.dataclass(slots=True, weakref_slot=True)
71
67
  class MegaAPI:
72
- session_id: str | None
73
- _request_id: int
74
- _client_id: str
68
+ _session: aiohttp.ClientSession | None = None
69
+
70
+ user_agent: str = f"{_package_name_}/{__version__}"
75
71
 
76
- __session: aiohttp.ClientSession | None
77
- _auto_close_session: bool
78
- _rate_limiter: AsyncLimiter
72
+ session_id: str | None = dataclasses.field(init=False, default=None)
73
+ _request_id: int = dataclasses.field(init=False, default_factory=random_u32int)
74
+ _client_id: str = dataclasses.field(init=False, default_factory=lambda: random_id(10))
79
75
 
80
- _entrypoint: ClassVar[yarl.URL] = yarl.URL("https://g.api.mega.co.nz/cs")
76
+ _auto_close_session: bool = dataclasses.field(init=False)
77
+ _rate_limiter: AsyncLimiter = dataclasses.field(init=False, default_factory=lambda: AsyncLimiter(100, 60))
81
78
 
82
- def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
83
- self.session_id = None
84
- self._request_id = random_u32int()
85
- self._client_id = random_id(10)
86
- self.__session = session
87
- self._auto_close_session = session is None
88
- self._rate_limiter = AsyncLimiter(100, 60)
79
+ _entrypoint: ClassVar[yarl.URL] = dataclasses.field(init=False, default=yarl.URL("https://g.api.mega.co.nz/cs"))
80
+
81
+ def __post_init__(self) -> None:
82
+ self._auto_close_session = self._session is None
89
83
 
90
84
  @property
91
85
  def entrypoint(self) -> yarl.URL:
@@ -95,22 +89,12 @@ class MegaAPI:
95
89
  def client_id(self) -> str:
96
90
  return self._client_id
97
91
 
98
- @property
99
- def request_id(self) -> int:
100
- return self._request_id
101
-
102
- @property
103
- def _session(self) -> aiohttp.ClientSession:
104
- if self.__session is None:
105
- self.__session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60))
106
- return self.__session
107
-
108
92
  def __repr__(self) -> str:
109
93
  return f"<{type(self).__name__}>(session_id={self.session_id!r}, client_id={self.client_id!r}, auto_close_session={self._auto_close_session!r})"
110
94
 
111
95
  async def aclose(self) -> None:
112
- if self._auto_close_session and self.__session:
113
- await self.__session.close()
96
+ if self._auto_close_session and self._session:
97
+ await self._session.close()
114
98
 
115
99
  async def __enter__(self) -> Self:
116
100
  return self
@@ -174,19 +158,40 @@ class MegaAPI:
174
158
  headers: Mapping[str, str] | None = None,
175
159
  **kwargs: Any,
176
160
  ) -> AsyncGenerator[aiohttp.ClientResponse]:
177
- kwargs["headers"] = _DEFAULT_HEADERS | (headers or {})
161
+ kwargs["headers"] = {"User-Agent": self.user_agent, **(headers or {})}
162
+ request_id = str(uuid.uuid4())
178
163
  if LOG_HTTP_TRAFFIC.get():
179
- params = ", ".join(f"{name} = {value!r}" for name, value in kwargs.items())
180
- logger.debug(f"Making {method} request to {url!s} with {params}")
181
- async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp:
182
- yield resp
164
+ logger.debug(
165
+ "Starting %s request [id=%s] to %s \n%s",
166
+ method,
167
+ request_id,
168
+ url,
169
+ kwargs,
170
+ )
171
+
172
+ resp = None
173
+ if self._session is None:
174
+ self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60))
175
+
176
+ try:
177
+ async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp:
178
+ yield resp
179
+ except RetryRequestError:
180
+ logger.warning("Request [id=%s] failed, retrying", request_id)
181
+ raise
182
+ finally:
183
+ if resp and LOG_HTTP_TRAFFIC.get():
184
+ logger.debug(
185
+ "Finished %s request [id=%s]\n%s",
186
+ method,
187
+ request_id,
188
+ _LazyResponseLog(resp),
189
+ )
183
190
 
184
191
  @staticmethod
185
192
  async def _parse_response(response: aiohttp.ClientResponse) -> Any:
186
193
  json_resp = await response.json()
187
194
  resp = json_resp
188
- if LOG_HTTP_TRAFFIC.get():
189
- logger.debug(f"Got response [{response.status}] json={json_resp!r}")
190
195
 
191
196
  if isinstance(json_resp, list) and len(json_resp) == 1:
192
197
  resp = json_resp[0]
@@ -196,27 +201,48 @@ class MegaAPI:
196
201
  return resp
197
202
 
198
203
  if resp == -3:
199
- msg = "Request failed, retrying"
200
- logger.warning(msg)
201
204
  raise RetryRequestError
202
205
  raise RequestError(resp)
203
206
 
204
207
  return resp
205
208
 
206
209
 
207
- class APIContextManager:
208
- __slots__ = ("_api",)
210
+ class _LazyResponseLog:
211
+ def __init__(self, resp: aiohttp.ClientResponse) -> None:
212
+ self.resp = resp
213
+
214
+ def __json__(self) -> dict[str, Any]:
215
+ me = {
216
+ "url": str(self.resp.url),
217
+ "status_code": self.resp.status,
218
+ "response_headers": dict(self.resp.headers),
219
+ "content": None,
220
+ }
221
+ if self.resp._body:
222
+ stripped = self.resp._body.strip()
223
+ if stripped:
224
+ content = json.loads(stripped.decode(self.resp.get_encoding()))
225
+ me.update(content=content)
226
+
227
+ return me
228
+
229
+ def __str__(self) -> str:
230
+ return str(self.__json__())
231
+
209
232
 
210
- def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
211
- self._api: MegaAPI = MegaAPI(session)
233
+ _API_T = TypeVar("_API_T", bound=MegaAPI, covariant=True)
234
+
235
+
236
+ class APIContextManager(Generic[_API_T]):
237
+ __slots__ = ("_api",)
212
238
 
213
239
  async def __aenter__(self) -> Self:
214
240
  return self
215
241
 
216
242
  async def __aexit__(self, *_) -> None:
217
- await self.close()
243
+ await self.aclose()
218
244
 
219
245
  async def aclose(self) -> None:
220
- await self._api.close()
246
+ await self._api.aclose()
221
247
 
222
248
  close = aclose
@@ -7,7 +7,7 @@ from pathlib import Path, PurePosixPath
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from mega import progress
10
- from mega.api import APIContextManager
10
+ from mega.api import APIContextManager, MegaAPI
11
11
  from mega.core import MegaCore
12
12
  from mega.crypto import a32_to_base64, b64_to_a32, b64_url_encode, encrypt_attr, encrypt_key
13
13
  from mega.data_structures import (
@@ -38,13 +38,16 @@ logger = logging.getLogger(__name__)
38
38
  _DOMAIN = Site.MEGA.value
39
39
 
40
40
 
41
- class MegaNzClient(APIContextManager):
41
+ class MegaNzClient(APIContextManager[MegaAPI]):
42
42
  """Interface with all the public methods of the API"""
43
43
 
44
44
  __slots__ = ("_core",)
45
45
 
46
- def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
47
- super().__init__(session)
46
+ def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None:
47
+ self._api = MegaAPI(session)
48
+ if user_agent:
49
+ self._api.user_agent = user_agent
50
+
48
51
  self._core: MegaCore = MegaCore(self._api)
49
52
  if hasattr(sys, "ps1"):
50
53
  setup_logger(logging.DEBUG)
@@ -134,6 +134,9 @@ class _DictDumper:
134
134
  """Get a JSONable dict representation of this object"""
135
135
  return dataclasses.asdict(self)
136
136
 
137
+ def __json__(self) -> dict[str, Any]:
138
+ return self.dump()
139
+
137
140
  def _shallow_dump(self) -> dict[str, Any]:
138
141
  return {name: getattr(self, name) for name in _fields(type(self))}
139
142
 
@@ -45,9 +45,13 @@ class TransferItAPI(MegaAPI):
45
45
  return await super().post(json, params)
46
46
 
47
47
 
48
- class TransferItClient(APIContextManager):
49
- def __init__(self, session: aiohttp.ClientSession | None = None) -> None: # pyright: ignore[reportMissingSuperCall]
48
+ class TransferItClient(APIContextManager[TransferItAPI]):
49
+ __slots__ = ()
50
+
51
+ def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None:
50
52
  self._api = TransferItAPI(session)
53
+ if user_agent:
54
+ self._api.user_agent = user_agent
51
55
 
52
56
  @property
53
57
  def progress_bar(self) -> _GeneratorContextManager[None, None, None]:
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Literal, TypeVar, overload
14
14
  import aiohttp
15
15
  import yarl
16
16
 
17
+ from mega import LOG_FILE_PROGRESS
17
18
  from mega.errors import ValidationError
18
19
 
19
20
  if TYPE_CHECKING:
@@ -63,7 +64,7 @@ def setup_logger(level: int = logging.INFO) -> None:
63
64
 
64
65
 
65
66
  def progress_logger(output_path: Path, file_size: int, *, download: bool) -> Callable[[float], None]:
66
- if not logger.isEnabledFor(10):
67
+ if not (LOG_FILE_PROGRESS.get() and logger.isEnabledFor(logging.DEBUG)):
67
68
  return lambda _: None
68
69
 
69
70
  from mega.data_structures import ByteSize
@@ -1,4 +0,0 @@
1
- import importlib.metadata
2
-
3
- _package_name_ = "async-mega-py"
4
- __version__ = importlib.metadata.version(_package_name_)
File without changes
File without changes