From b69124ab130f773e4e93dd81196fa841d51e66f6 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:01:02 -0500 Subject: [PATCH 1/6] refactor: update API class --- src/mega/api.py | 70 ++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/mega/api.py b/src/mega/api.py index 6a12a46..ec13cb3 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -66,27 +66,23 @@ async def inner_wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: return wrapper -@dataclasses.dataclass(slots=True, weakref_slot=True, init=False) +@dataclasses.dataclass(slots=True, weakref_slot=True) class MegaAPI: - session_id: str | None - _request_id: int - _client_id: str - - __session: aiohttp.ClientSession | None - _auto_close_session: bool - _rate_limiter: AsyncLimiter - - _entrypoint: ClassVar[yarl.URL] = yarl.URL("https://g.api.mega.co.nz/cs") - user_agent: str - - def __init__(self, session: aiohttp.ClientSession | None = None, user_agent: str | None = None) -> None: - self.session_id = None - self._request_id = random_u32int() - self._client_id = random_id(10) - self.__session = session - self._auto_close_session = session is None - self._rate_limiter = AsyncLimiter(100, 60) - self.user_agent = user_agent or f"{_package_name_}/{__version__}" + _session: aiohttp.ClientSession | None = None + + user_agent: str = f"{_package_name_}/{__version__}" + + session_id: str | None = dataclasses.field(init=False, default=None) + _request_id: int = dataclasses.field(init=False, default_factory=random_u32int) + _client_id: str = dataclasses.field(init=False, default_factory=lambda: random_id(10)) + + _auto_close_session: bool = dataclasses.field(init=False) + _rate_limiter: AsyncLimiter = dataclasses.field(init=False, default_factory=lambda: AsyncLimiter(100, 60)) + + _entrypoint: ClassVar[yarl.URL] = dataclasses.field(init=False, default=yarl.URL("https://g.api.mega.co.nz/cs")) + + def __post_init__(self) -> None: + self._auto_close_session = self._session is None @property def entrypoint(self) -> yarl.URL: @@ -97,21 +93,17 @@ def client_id(self) -> str: return self._client_id @property - def request_id(self) -> int: - return self._request_id - - @property - def _session(self) -> aiohttp.ClientSession: - if self.__session is None: - self.__session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60)) - return self.__session + def session(self) -> aiohttp.ClientSession: + if self._session is None: + self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60)) + return self._session def __repr__(self) -> str: 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})" async def aclose(self) -> None: - if self._auto_close_session and self.__session: - await self.__session.close() + if self._auto_close_session and self._session: + await self._session.close() async def __enter__(self) -> Self: return self @@ -175,7 +167,7 @@ async def __request( headers: Mapping[str, str] | None = None, **kwargs: Any, ) -> AsyncGenerator[aiohttp.ClientResponse]: - kwargs["headers"] = {"User-Agent": self.user_agent} | (headers or {}) + kwargs["headers"] = {"User-Agent": self.user_agent, **(headers or {})} request_id = str(uuid.uuid4()) if LOG_HTTP_TRAFFIC.get(): logger.debug( @@ -188,7 +180,7 @@ async def __request( resp = None try: - async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp: + async with self._rate_limiter, self.session.request(method, url, **kwargs) as resp: yield resp except RetryRequestError: logger.warning("Request [id=%s] failed, retrying", request_id) @@ -247,14 +239,22 @@ def __str__(self) -> str: class APIContextManager: __slots__ = ("_api",) - def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: - self._api: MegaAPI = MegaAPI(session, user_agent) + def __init__(self, session: aiohttp.ClientSession | None = None) -> None: + self._api: MegaAPI = MegaAPI(session) + + @property + def user_agent(self) -> str: + return self._api.user_agent + + @user_agent.setter + def user_agent(self, ua: str) -> None: + self._api.user_agent = ua async def __aenter__(self) -> Self: return self async def __aexit__(self, *_) -> None: - await self.close() + await self.aclose() async def aclose(self) -> None: await self._api.close() From 0c4508b82b80795487cdb1427100e31ac6dc7d33 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:12:24 -0500 Subject: [PATCH 2/6] refactor: make APIcontextmanager generic --- src/mega/api.py | 18 +++++------------- src/mega/client.py | 9 ++++++--- src/mega/transfer_it.py | 8 +++++--- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/mega/api.py b/src/mega/api.py index ec13cb3..e9ae7fe 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -9,7 +9,7 @@ from collections.abc import Mapping, Sequence from contextvars import ContextVar from functools import wraps -from typing import TYPE_CHECKING, Any, ClassVar, Literal, ParamSpec, Self, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, Self, TypeVar import aiohttp import yarl @@ -236,19 +236,11 @@ def __str__(self) -> str: return str(self.__json__()) -class APIContextManager: - __slots__ = ("_api",) - - def __init__(self, session: aiohttp.ClientSession | None = None) -> None: - self._api: MegaAPI = MegaAPI(session) +_API_T = TypeVar("_API_T", bound=MegaAPI, covariant=True) - @property - def user_agent(self) -> str: - return self._api.user_agent - @user_agent.setter - def user_agent(self, ua: str) -> None: - self._api.user_agent = ua +class APIContextManager(Generic[_API_T]): + __slots__ = ("_api",) async def __aenter__(self) -> Self: return self @@ -257,6 +249,6 @@ async def __aexit__(self, *_) -> None: await self.aclose() async def aclose(self) -> None: - await self._api.close() + await self._api.aclose() close = aclose diff --git a/src/mega/client.py b/src/mega/client.py index 2c2dfbb..3eec57d 100644 --- a/src/mega/client.py +++ b/src/mega/client.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from mega import progress -from mega.api import APIContextManager +from mega.api import APIContextManager, MegaAPI from mega.core import MegaCore from mega.crypto import a32_to_base64, b64_to_a32, b64_url_encode, encrypt_attr, encrypt_key from mega.data_structures import ( @@ -38,13 +38,16 @@ _DOMAIN = Site.MEGA.value -class MegaNzClient(APIContextManager): +class MegaNzClient(APIContextManager[MegaAPI]): """Interface with all the public methods of the API""" __slots__ = ("_core",) def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: - super().__init__(session, user_agent=user_agent) + self._api = MegaAPI(session) + if user_agent: + self._api.user_agent = user_agent + self._core: MegaCore = MegaCore(self._api) if hasattr(sys, "ps1"): setup_logger(logging.DEBUG) diff --git a/src/mega/transfer_it.py b/src/mega/transfer_it.py index a0bb1da..9d0bb0d 100644 --- a/src/mega/transfer_it.py +++ b/src/mega/transfer_it.py @@ -45,9 +45,11 @@ async def post(self, json: dict[str, Any] | list[dict[str, Any]], params: dict[s return await super().post(json, params) -class TransferItClient(APIContextManager): - def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: # pyright: ignore[reportMissingSuperCall] - self._api = TransferItAPI(session, user_agent=user_agent) +class TransferItClient(APIContextManager[TransferItAPI]): + def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: + self._api = TransferItAPI(session) + if user_agent: + self._api.user_agent = user_agent @property def progress_bar(self) -> _GeneratorContextManager[None, None, None]: From 90b7606848c6db6f28c8e7b14d0b3d86f180c96c Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:18:27 -0500 Subject: [PATCH 3/6] refactor: session should still be private --- src/mega/api.py | 11 ++++------- src/mega/transfer_it.py | 2 ++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/mega/api.py b/src/mega/api.py index e9ae7fe..8d89880 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -92,12 +92,6 @@ def entrypoint(self) -> yarl.URL: def client_id(self) -> str: return self._client_id - @property - def session(self) -> aiohttp.ClientSession: - if self._session is None: - self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60)) - return self._session - def __repr__(self) -> str: 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})" @@ -179,8 +173,11 @@ async def __request( ) resp = None + if self._session is None: + self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=160, sock_read=60)) + try: - async with self._rate_limiter, self.session.request(method, url, **kwargs) as resp: + async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp: yield resp except RetryRequestError: logger.warning("Request [id=%s] failed, retrying", request_id) diff --git a/src/mega/transfer_it.py b/src/mega/transfer_it.py index 9d0bb0d..deea1a4 100644 --- a/src/mega/transfer_it.py +++ b/src/mega/transfer_it.py @@ -46,6 +46,8 @@ async def post(self, json: dict[str, Any] | list[dict[str, Any]], params: dict[s class TransferItClient(APIContextManager[TransferItAPI]): + __slots__ = () + def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: self._api = TransferItAPI(session) if user_agent: From 8633fd6efa6a0455656640abafd513013b83ac87 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:21:00 -0500 Subject: [PATCH 4/6] feat: allow disabling progress logs --- src/mega/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mega/utils.py b/src/mega/utils.py index e61cc50..bc79275 100644 --- a/src/mega/utils.py +++ b/src/mega/utils.py @@ -7,6 +7,7 @@ import random import string from collections.abc import Callable +from contextvars import ContextVar from enum import Enum from stat import S_ISREG from typing import TYPE_CHECKING, Literal, TypeVar, overload @@ -23,7 +24,7 @@ _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") - +LOG_PROGRESS: ContextVar[bool] = ContextVar("LOG_PROGRESS", default=True) logger = logging.getLogger(__name__) @@ -63,7 +64,7 @@ def setup_logger(level: int = logging.INFO) -> None: def progress_logger(output_path: Path, file_size: int, *, download: bool) -> Callable[[float], None]: - if not logger.isEnabledFor(10): + if not (LOG_PROGRESS.get() and logger.isEnabledFor(logging.DEBUG)): return lambda _: None from mega.data_structures import ByteSize From 79e8d0a839d683b61a68a69e8b205d019c0666d8 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:25:56 -0500 Subject: [PATCH 5/6] refactor: make log context vars easier to discover --- src/mega/__init__.py | 4 ++++ src/mega/__main__.py | 3 +-- src/mega/api.py | 5 +---- src/mega/utils.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/mega/__init__.py b/src/mega/__init__.py index e98150a..8b92a6f 100644 --- a/src/mega/__init__.py +++ b/src/mega/__init__.py @@ -1,4 +1,8 @@ import importlib.metadata +from contextvars import ContextVar _package_name_ = "async-mega-py" __version__ = importlib.metadata.version(_package_name_) + +LOG_FILE_PROGRESS: ContextVar[bool] = ContextVar("LOG_PROGRESS", default=True) +LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False) diff --git a/src/mega/__main__.py b/src/mega/__main__.py index d8c7085..d872241 100644 --- a/src/mega/__main__.py +++ b/src/mega/__main__.py @@ -14,8 +14,7 @@ import yarl from cyclopts import App, Parameter -from mega import __version__, env -from mega.api import LOG_HTTP_TRAFFIC +from mega import LOG_HTTP_TRAFFIC, __version__, env from mega.client import MegaNzClient from mega.transfer_it import TransferItClient from mega.utils import Site, setup_logger diff --git a/src/mega/api.py b/src/mega/api.py index 8d89880..31e5de0 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -7,7 +7,6 @@ import logging import uuid from collections.abc import Mapping, Sequence -from contextvars import ContextVar from functools import wraps from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, Self, TypeVar @@ -15,7 +14,7 @@ import yarl from aiolimiter import AsyncLimiter -from mega import __version__, _package_name_ +from mega import LOG_HTTP_TRAFFIC, __version__, _package_name_ from mega.crypto import generate_hashcash from mega.errors import RequestError, RetryRequestError from mega.utils import random_id, random_u32int @@ -27,8 +26,6 @@ _R = TypeVar("_R") -LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False) - logger = logging.getLogger(__name__) diff --git a/src/mega/utils.py b/src/mega/utils.py index bc79275..80258a3 100644 --- a/src/mega/utils.py +++ b/src/mega/utils.py @@ -7,7 +7,6 @@ import random import string from collections.abc import Callable -from contextvars import ContextVar from enum import Enum from stat import S_ISREG from typing import TYPE_CHECKING, Literal, TypeVar, overload @@ -15,6 +14,7 @@ import aiohttp import yarl +from mega import LOG_FILE_PROGRESS from mega.errors import ValidationError if TYPE_CHECKING: @@ -24,7 +24,7 @@ _T1 = TypeVar("_T1") _T2 = TypeVar("_T2") -LOG_PROGRESS: ContextVar[bool] = ContextVar("LOG_PROGRESS", default=True) + logger = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def setup_logger(level: int = logging.INFO) -> None: def progress_logger(output_path: Path, file_size: int, *, download: bool) -> Callable[[float], None]: - if not (LOG_PROGRESS.get() and logger.isEnabledFor(logging.DEBUG)): + if not (LOG_FILE_PROGRESS.get() and logger.isEnabledFor(logging.DEBUG)): return lambda _: None from mega.data_structures import ByteSize From 9368c37e101ed6c13806dd5a0f99c3372e721778 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:27:41 -0500 Subject: [PATCH 6/6] chore: bump version --- pyproject.toml | 2 +- src/mega/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 14efa9f..11a5935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ license = "Apache-2.0" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" -version = "2.2.0" +version = "2.2.1" [project.optional-dependencies] cli = [ diff --git a/src/mega/__init__.py b/src/mega/__init__.py index 8b92a6f..f5cb928 100644 --- a/src/mega/__init__.py +++ b/src/mega/__init__.py @@ -4,5 +4,5 @@ _package_name_ = "async-mega-py" __version__ = importlib.metadata.version(_package_name_) -LOG_FILE_PROGRESS: ContextVar[bool] = ContextVar("LOG_PROGRESS", default=True) +LOG_FILE_PROGRESS: ContextVar[bool] = ContextVar("LOG_FILE_PROGRESS", default=True) LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False) diff --git a/uv.lock b/uv.lock index d9fc671..ac2fe27 100644 --- a/uv.lock +++ b/uv.lock @@ -137,7 +137,7 @@ wheels = [ [[package]] name = "async-mega-py" -version = "2.2.0" +version = "2.2.1" source = { editable = "." } dependencies = [ { name = "aiohttp" },