From 7144af57113d3bdd65bf8af144a70e0c691a903f Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:35:46 -0500 Subject: [PATCH 1/5] refactor: support a custom user agent --- src/mega/api.py | 15 ++++++--------- src/mega/client.py | 4 ++-- src/mega/transfer_it.py | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/mega/api.py b/src/mega/api.py index 07cc3df..9765b69 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -7,13 +7,13 @@ from collections.abc import Mapping, Sequence from contextvars import ContextVar from functools import wraps -from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, Literal, ParamSpec, Self, TypeVar import aiohttp import yarl from aiolimiter import AsyncLimiter +from mega import __version__, _package_name_ from mega.crypto import generate_hashcash from mega.errors import RequestError, RetryRequestError from mega.utils import random_id, random_u32int @@ -25,10 +25,6 @@ _R = TypeVar("_R") -_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0" -_DEFAULT_HEADERS: MappingProxyType[str, str] = MappingProxyType({"User-Agent": _UA}) - - LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False) logger = logging.getLogger(__name__) @@ -79,13 +75,14 @@ class MegaAPI: _entrypoint: ClassVar[yarl.URL] = yarl.URL("https://g.api.mega.co.nz/cs") - def __init__(self, session: aiohttp.ClientSession | None = None) -> None: + 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__}" @property def entrypoint(self) -> yarl.URL: @@ -174,7 +171,7 @@ async def __request( headers: Mapping[str, str] | None = None, **kwargs: Any, ) -> AsyncGenerator[aiohttp.ClientResponse]: - kwargs["headers"] = _DEFAULT_HEADERS | (headers or {}) + kwargs["headers"] = {"User-Agent": self.user_agent} | (headers or {}) if LOG_HTTP_TRAFFIC.get(): params = ", ".join(f"{name} = {value!r}" for name, value in kwargs.items()) logger.debug(f"Making {method} request to {url!s} with {params}") @@ -207,8 +204,8 @@ async def _parse_response(response: aiohttp.ClientResponse) -> Any: class APIContextManager: __slots__ = ("_api",) - def __init__(self, session: aiohttp.ClientSession | None = None) -> None: - self._api: MegaAPI = MegaAPI(session) + def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: + self._api: MegaAPI = MegaAPI(session, user_agent) async def __aenter__(self) -> Self: return self diff --git a/src/mega/client.py b/src/mega/client.py index d997074..c7d9f45 100644 --- a/src/mega/client.py +++ b/src/mega/client.py @@ -43,8 +43,8 @@ class MegaNzClient(APIContextManager): __slots__ = ("_core",) - def __init__(self, session: aiohttp.ClientSession | None = None) -> None: - super().__init__(session) + def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: + super().__init__(session, 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 5b42c52..ad26d50 100644 --- a/src/mega/transfer_it.py +++ b/src/mega/transfer_it.py @@ -46,8 +46,8 @@ async def post(self, json: dict[str, Any] | list[dict[str, Any]], params: dict[s class TransferItClient(APIContextManager): - def __init__(self, session: aiohttp.ClientSession | None = None) -> None: # pyright: ignore[reportMissingSuperCall] - self._api = TransferItAPI(session) + def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: # pyright: ignore[reportMissingSuperCall] + self._api = TransferItAPI(session, user_agent) @property def progress_bar(self) -> _GeneratorContextManager[None, None, None]: From b2e417e69b5b7accf4ed55e8e52a042de7e8d759 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:34:07 -0500 Subject: [PATCH 2/5] refactor: improve http logging --- src/mega/api.py | 58 +++++++++++++++++++++++++++++++++++------ src/mega/client.py | 2 +- src/mega/transfer_it.py | 2 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/mega/api.py b/src/mega/api.py index 9765b69..8587344 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -3,7 +3,9 @@ import asyncio import contextlib import dataclasses +import json import logging +import uuid from collections.abc import Mapping, Sequence from contextvars import ContextVar from functools import wraps @@ -26,6 +28,7 @@ LOG_HTTP_TRAFFIC: ContextVar[bool] = ContextVar("LOG_HTTP_TRAFFIC", default=False) + logger = logging.getLogger(__name__) @@ -172,18 +175,36 @@ async def __request( **kwargs: Any, ) -> AsyncGenerator[aiohttp.ClientResponse]: kwargs["headers"] = {"User-Agent": self.user_agent} | (headers or {}) + request_id = str(uuid.uuid4()) if LOG_HTTP_TRAFFIC.get(): - params = ", ".join(f"{name} = {value!r}" for name, value in kwargs.items()) - logger.debug(f"Making {method} request to {url!s} with {params}") - async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp: - yield resp + logger.debug( + "Starting %s request [id=%s] to %s \n%s", + method, + request_id, + url, + kwargs, + ) + + resp = None + try: + 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) + raise + finally: + if resp and LOG_HTTP_TRAFFIC.get(): + logger.debug( + "Finished %s request [id=%s]\n%s", + method, + request_id, + _LazyResponseLog(resp), + ) @staticmethod async def _parse_response(response: aiohttp.ClientResponse) -> Any: json_resp = await response.json() resp = json_resp - if LOG_HTTP_TRAFFIC.get(): - logger.debug(f"Got response [{response.status}] json={json_resp!r}") if isinstance(json_resp, list) and len(json_resp) == 1: resp = json_resp[0] @@ -193,14 +214,35 @@ async def _parse_response(response: aiohttp.ClientResponse) -> Any: return resp if resp == -3: - msg = "Request failed, retrying" - logger.warning(msg) raise RetryRequestError raise RequestError(resp) return resp +class _LazyResponseLog: + def __init__(self, resp: aiohttp.ClientResponse) -> None: + self.resp = resp + + def __json__(self) -> dict[str, Any]: + me = { + "url": str(self.resp.url), + "status_code": self.resp.status, + "response_headers": dict(self.resp.headers), + "content": None, + } + if self.resp._body: + stripped = self.resp._body.strip() + if stripped: + content = json.loads(stripped.decode(self.resp.get_encoding())) + me.update(content=content) + + return me + + def __str__(self) -> str: + return str(self.__json__()) + + class APIContextManager: __slots__ = ("_api",) diff --git a/src/mega/client.py b/src/mega/client.py index c7d9f45..2c2dfbb 100644 --- a/src/mega/client.py +++ b/src/mega/client.py @@ -44,7 +44,7 @@ class MegaNzClient(APIContextManager): __slots__ = ("_core",) def __init__(self, session: aiohttp.ClientSession | None = None, *, user_agent: str | None = None) -> None: - super().__init__(session, user_agent) + super().__init__(session, 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 ad26d50..a0bb1da 100644 --- a/src/mega/transfer_it.py +++ b/src/mega/transfer_it.py @@ -47,7 +47,7 @@ async def post(self, json: dict[str, Any] | list[dict[str, Any]], params: dict[s 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) + self._api = TransferItAPI(session, user_agent=user_agent) @property def progress_bar(self) -> _GeneratorContextManager[None, None, None]: From f8f07ad23293f8e07c83594b020d0b06b581600b Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:38:42 -0500 Subject: [PATCH 3/5] fix: user agent attr --- src/mega/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mega/api.py b/src/mega/api.py index 8587344..6a12a46 100644 --- a/src/mega/api.py +++ b/src/mega/api.py @@ -77,6 +77,7 @@ class MegaAPI: _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 From b9ef06c0916e77834e455c73d69fb9701f0f54d7 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:46:48 -0500 Subject: [PATCH 4/5] refactor: add json method to DictDumper --- src/mega/data_structures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mega/data_structures.py b/src/mega/data_structures.py index 431ec98..475ed82 100644 --- a/src/mega/data_structures.py +++ b/src/mega/data_structures.py @@ -134,6 +134,9 @@ def dump(self) -> dict[str, Any]: """Get a JSONable dict representation of this object""" return dataclasses.asdict(self) + def __json__(self) -> dict[str, Any]: + return self.dump() + def _shallow_dump(self) -> dict[str, Any]: return {name: getattr(self, name) for name in _fields(type(self))} From f2543815def7129831af487a6cc90de4df074f04 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sun, 26 Apr 2026 08:54:28 -0500 Subject: [PATCH 5/5] chore: bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b00e15d..14efa9f 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.1.1" +version = "2.2.0" [project.optional-dependencies] cli = [