Metadata-Version: 2.4
Name: mega-public-client
Version: 0.1.1
Summary: Modern sync and async Python client for MEGA public file and folder links.
Author: keuryn
License: MIT
License-File: LICENSE
Keywords: async,cryptography,download,httpx,mega
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.25
Requires-Dist: pycryptodome>=3.19
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# mega-public-client

`mega-public-client` is a modern Python 3.11+ library for downloading files and
folders from public MEGA links.

It provides synchronous and asynchronous APIs, local MEGA decryption, resumable
chunked downloads, progress callbacks, folder traversal, custom retry handling,
and clean typed models.

The library does **not** depend on abandoned MEGA wrappers and does **not** use
`tenacity`. Runtime dependencies are intentionally small:

- `httpx`
- `pycryptodome`

## Features

- Public MEGA file downloads
- Public MEGA folder downloads
- Original file names from MEGA metadata
- Recursive folder paths preserved on disk
- Sync API via `MegaClient`
- Async API via `AsyncMegaClient`
- Parallel chunk downloads
- Resume support through `.part` files
- Streaming helpers for small files
- Progress callbacks
- Custom sync and async retry engine
- Exponential backoff with retry filters
- MEGA AES-CTR file decryption
- MEGA AES-CBC attribute decryption
- Modern public URL parser
- Structured exception hierarchy
- Dataclasses for public models
- Type hints and `py.typed`
- PyPI-ready `pyproject.toml`
- Offline unit tests for parser, crypto, retry, and path behavior

## Installation

From PyPI, after publishing:

```bash
pip install mega-public-client
```

From a local checkout:

```bash
pip install .
```

For development, install in editable mode:

```bash
pip install -e .
```

With test dependencies:

```bash
pip install -e ".[test]"
```

## Quick Start

Download one public file using the original MEGA file name:

```python
from mega_client import MegaClient

url = "https://mega.nz/file/FILE_ID#FILE_KEY"

with MegaClient() as client:
    result = client.download_file(url, "downloads")

print(result.path)
```

If the MEGA file is named `movie.mp4`, it will be saved as:

```text
downloads/movie.mp4
```

To force a custom output name, pass a full file path:

```python
from mega_client import MegaClient

with MegaClient() as client:
    client.download_file(
        "https://mega.nz/file/FILE_ID#FILE_KEY",
        "downloads/custom-name.mp4",
    )
```

## Download a Folder

Use `download_folder()` with a public folder URL:

```python
from mega_client import MegaClient

url = "https://mega.nz/folder/FOLDER_ID#FOLDER_KEY"

with MegaClient() as client:
    results = client.download_folder(url, "downloads")

for item in results:
    print(item.path)
```

The library preserves folder paths returned by MEGA. For example, a public
folder containing:

```text
Videos/
  intro.mp4
  lessons/
    part-1.mp4
```

will be written as:

```text
downloads/Videos/intro.mp4
downloads/Videos/lessons/part-1.mp4
```

## Async Usage

Use `AsyncMegaClient` with `async with`:

```python
import asyncio

from mega_client import AsyncMegaClient


async def main() -> None:
    async with AsyncMegaClient() as client:
        result = await client.download_file(
            "https://mega.nz/file/FILE_ID#FILE_KEY",
            "downloads",
        )
        print(result.path)


asyncio.run(main())
```

Async folder download:

```python
import asyncio

from mega_client import AsyncMegaClient


async def main() -> None:
    async with AsyncMegaClient() as client:
        results = await client.download_folder(
            "https://mega.nz/folder/FOLDER_ID#FOLDER_KEY",
            "downloads",
        )

    for result in results:
        print(result.path)


asyncio.run(main())
```

## Progress Callbacks

Progress callbacks receive two arguments:

- `done`: bytes downloaded and decrypted so far
- `total`: total bytes when known

Synchronous example:

```python
from mega_client import MegaClient


def progress(done: int, total: int | None) -> None:
    if total:
        percent = done / total * 100
        print(f"{percent:.1f}% ({done}/{total})")
    else:
        print(done)


with MegaClient() as client:
    client.download_file(
        "https://mega.nz/file/FILE_ID#FILE_KEY",
        "downloads",
        progress=progress,
    )
```

Async progress callbacks may be regular functions or async functions:

```python
import asyncio

from mega_client import AsyncMegaClient


async def progress(done: int, total: int | None) -> None:
    print(done, total)


async def main() -> None:
    async with AsyncMegaClient() as client:
        await client.download_file(
            "https://mega.nz/file/FILE_ID#FILE_KEY",
            "downloads",
            progress=progress,
        )


asyncio.run(main())
```

## Streaming Small Files

For small files, you can download and decrypt directly into memory:

```python
from mega_client import MegaClient

with MegaClient() as client:
    data = client.stream_file("https://mega.nz/file/FILE_ID#FILE_KEY")

print(len(data))
```

Async:

```python
import asyncio

from mega_client import AsyncMegaClient


async def main() -> None:
    async with AsyncMegaClient() as client:
        data = await client.stream_file("https://mega.nz/file/FILE_ID#FILE_KEY")
        print(len(data))


asyncio.run(main())
```

For large videos and archives, prefer `download_file()` so the library writes
chunks to disk instead of keeping the whole file in memory.

## Resume Support

Downloads are written to a temporary `.part` file first. If a download is
interrupted, call `download_file()` again with the same destination and the
library will resume from the existing `.part` file when possible.

Resume is enabled by default:

```python
client.download_file(url, "downloads", resume=True)
```

Disable resume:

```python
client.download_file(url, "downloads", resume=False)
```

Resume offsets are aligned to AES block boundaries so decryption can continue
correctly.

## Chunk Size and Parallelism

You can tune chunk size and request concurrency:

```python
from mega_client import MegaClient

with MegaClient() as client:
    client.download_file(
        "https://mega.nz/file/FILE_ID#FILE_KEY",
        "downloads",
        chunk_size=2 * 1024 * 1024,
        parallelism=6,
    )
```

Defaults:

- `chunk_size`: `1 MiB`
- `parallelism`: `4`

Higher parallelism may improve throughput, but it can also trigger server-side
rate limits or increase memory and disk pressure.

## URL Parsing

The parser supports modern and legacy public MEGA links:

```python
from mega_client import parse_mega_url

link = parse_mega_url("https://mega.nz/file/FILE_ID#FILE_KEY")

print(link.type)
print(link.handle)
print(link.key)
```

Supported examples:

```text
https://mega.nz/file/<file_id>#<file_key>
https://mega.nz/folder/<folder_id>#<folder_key>
https://mega.nz/#F!<folder_id>!<folder_key>
https://mega.nz/#!<file_id>!<file_key>
```

Malformed or unsupported URLs raise `MegaInvalidURLError`.

## Metadata

Read file metadata without downloading:

```python
from mega_client import MegaClient

with MegaClient() as client:
    info = client.get_file_info("https://mega.nz/file/FILE_ID#FILE_KEY")

print(info.name)
print(info.size)
print(info.handle)
```

Read folder metadata:

```python
from mega_client import MegaClient

with MegaClient() as client:
    folder = client.get_folder_info("https://mega.nz/folder/FOLDER_ID#FOLDER_KEY")

for node in folder.nodes:
    print(node.name, node.is_file, node.is_folder)
```

## Exceptions

All package-specific exceptions inherit from `MegaClientError`.

```python
from mega_client import MegaClient, MegaClientError

try:
    with MegaClient() as client:
        client.download_file(url, "downloads")
except MegaClientError as exc:
    print(f"MEGA download failed: {exc}")
```

Exception classes:

- `MegaClientError`: base class
- `MegaInvalidURLError`: invalid or unsupported public URL
- `MegaAPIError`: MEGA API returned an error or malformed response
- `MegaCryptoError`: key, attribute, or decryption failure
- `MegaDownloadError`: download could not complete
- `MegaIntegrityError`: completed file failed validation
- `MegaRetryError`: retry loop exhausted unexpectedly

## Retry Configuration

The library includes its own retry engine for sync and async code.

```python
from mega_client import MegaClient
from mega_client.retry import RetryConfig

retry = RetryConfig(
    attempts=6,
    base_delay=0.5,
    factor=2.0,
    max_delay=15.0,
    jitter=0.2,
)

with MegaClient(retry=retry) as client:
    client.download_file(url, "downloads")
```

By default, retries are applied to transient network errors, timeouts, and
server-side HTTP responses such as `429`, `500`, `502`, `503`, and `504`.

Custom retry filter:

```python
from mega_client.retry import RetryConfig


def retry_filter(exc: BaseException) -> bool:
    return isinstance(exc, TimeoutError)


retry = RetryConfig(attempts=3, retry_filter=retry_filter)
```

## Logging

The package uses standard Python logging names:

- `mega_client.sync`
- `mega_client.async`
- `mega_client`

Example:

```python
import logging

logging.basicConfig(level=logging.INFO)
```

You can pass a custom logger:

```python
import logging

from mega_client import MegaClient

logger = logging.getLogger("my_app.mega")

with MegaClient(logger=logger) as client:
    client.download_file(url, "downloads")
```

## Public API

Main imports:

```python
from mega_client import MegaClient
from mega_client import AsyncMegaClient
from mega_client import parse_mega_url
```

Common models:

```python
from mega_client import FileInfo
from mega_client import FolderInfo
from mega_client import NodeInfo
from mega_client import DownloadResult
```

`DownloadResult` fields:

- `path`: final local path
- `bytes_written`: number of bytes written
- `resumed`: whether an existing `.part` file was used
- `verified`: whether final validation passed

`FileInfo` fields:

- `name`: original file name from MEGA metadata
- `size`: file size in bytes
- `download_url`: temporary MEGA download URL
- `key`: decoded MEGA file key material
- `handle`: MEGA file handle
- `attributes`: decrypted metadata attributes

`NodeInfo` fields:

- `handle`: MEGA node handle
- `parent`: parent node handle when present
- `kind`: MEGA node type
- `name`: decrypted node name
- `size`: file size for file nodes
- `key`: decrypted node key
- `attributes`: decrypted node attributes
- `is_file`: convenience property
- `is_folder`: convenience property

## MEGA Protocol Notes

Public-link operations use MEGA's command endpoint:

```text
https://g.api.mega.co.nz/cs
```

The client sends JSON arrays because the MEGA command server supports batched
commands.

Public file metadata is requested with:

```json
[
  {
    "a": "g",
    "g": 1,
    "p": "<file_handle>"
  }
]
```

Public folder trees are requested with the folder handle in the request query
parameter `n` and a folder listing command body:

```json
[
  {
    "a": "f",
    "c": 1,
    "r": 1,
    "ca": 1
  }
]
```

Downloads for nodes inside public folders also require the public folder handle
as API context.

MEGA files are encrypted client-side:

- Public file keys contain eight 32-bit words.
- The AES key is derived by XORing key words with nonce/MAC words.
- File content is decrypted with AES-CTR.
- File and folder attributes are decrypted with AES-CBC and a zero IV.
- Attribute plaintext begins with the literal `MEGA` prefix followed by JSON.

## Project Layout

```text
mega_client/
  client.py        # synchronous facade
  async_client.py  # asynchronous API and MEGA command calls
  downloader.py    # chunked download and resume logic
  crypto.py        # MEGA key and AES helpers
  parser.py        # public URL parser
  models.py        # dataclasses and enums
  retry.py         # custom retry engine
  exceptions.py    # exception hierarchy
  progress.py      # progress callback helpers
  constants.py     # defaults and protocol constants
  utils.py         # small shared helpers
  types.py         # callback and retry typing aliases
```

## Development

Install locally:

```bash
pip install -e ".[test]"
```

Run tests:

```bash
python -m pytest
```

Run the benchmark helper:

```bash
python benchmarks/download_throughput.py "https://mega.nz/file/FILE_ID#FILE_KEY" output.bin
```

## Examples

Example scripts are available in:

```text
examples/download_file.py
examples/download_file_async.py
```

## Limitations

- Only public MEGA file and folder links are supported.
- Account login, private cloud operations, uploads, deletes, and sharing
  management are outside the scope of this package.
- Integrity validation currently verifies completed file size and correct
  decryption flow. Full MEGA MAC verification can be added as an additional
  hardening step.
- `stream_file()` loads the entire decrypted file into memory and should only be
  used for small files.

## License

MIT. See [LICENSE](LICENSE).
