Skip to content

Commit 840ee4b

Browse files
authored
refactor: replace typer with cyclopts (#11)
* refactor: replace typer with cyclopts * refactor: use meta app for verbose * fix: rich docstring * fix: typo * chore: pin uv and ruff * docs: update readme * chore: bump version to v2.1.0
1 parent 8c72081 commit 840ee4b

10 files changed

Lines changed: 242 additions & 305 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
strategy:
2727
matrix:
28-
os: [ubuntu-latest, windows-latest, macos-latest]
28+
os: [ubuntu-latest, windows-latest]
2929
python-version: [3.11, 3.13]
3030
fail-fast: true
3131
defaults:
@@ -39,6 +39,7 @@ jobs:
3939
- name: Install uv
4040
uses: astral-sh/setup-uv@v6
4141
with:
42+
version: "0.10.11"
4243
python-version: ${{ matrix.python-version }}
4344

4445
- name: Install dependencies

.github/workflows/ruff.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ jobs:
55
runs-on: ubuntu-latest
66
steps:
77
- name: checkout code
8-
uses: actions/checkout@v4
8+
uses: actions/checkout@v5
99

1010
- name: ruff lint
1111
uses: astral-sh/ruff-action@v3
12+
with:
13+
version: "0.15.6"
1214

1315
- name: ruff format check
1416
uses: astral-sh/ruff-action@v3
1517
with:
16-
args: "format --check"
18+
version: "0.15.6"
19+
args:
20+
"format --check --diff"

.pre-commit-config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
33
rev: v6.0.0
44
hooks:
5+
- id: check-added-large-files
56
- id: check-case-conflict
7+
- id: check-illegal-windows-names
68
- id: check-merge-conflict
79
- id: check-toml
810
- id: check-yaml
@@ -20,7 +22,7 @@ repos:
2022
files: ^(.*\.toml)$
2123

2224
- repo: https://github.com/astral-sh/ruff-pre-commit
23-
rev: v0.15.0
25+
rev: v0.15.6
2426
hooks:
2527
- id: ruff
2628
args: [ --fix ]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ dict(fs.search("/tests/logo.png"))
291291
## Transfer.it
292292

293293
> [!NOTE]
294-
> The `transfer.it` client does not support uploads (yet!)
294+
> The `transfer.it` client does not support uploads yet (PRs welcome)
295295
296296
```python
297297
from mega.transfer_it import TransferItClient

pyproject.toml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,17 @@ license = "Apache-2.0"
3434
license-files = ["LICENSE"]
3535
readme = "README.md"
3636
requires-python = ">=3.11"
37-
version = "2.0.3"
37+
version = "2.1.0"
3838

3939
[project.optional-dependencies]
4040
cli = [
41-
"async-mega-py[default]",
42-
"typer-slim>=0.21.1"
43-
]
44-
default = [
45-
"python-dotenv>=1.2.1",
46-
"rich>=14.0.0"
41+
"cyclopts>=4.10.1",
42+
"python-dotenv>=1.2.1"
4743
]
4844

4945
[project.scripts]
50-
async-mega-py = "mega.cli:main"
51-
mega-py = "mega.cli:main"
46+
async-mega-py = "mega.__main__:app.meta"
47+
mega-py = "mega.__main__:app.meta"
5248

5349
[project.urls]
5450
Homepage = "https://github.com/NTFSvolume/mega.py"
@@ -123,14 +119,12 @@ module-name = "mega"
123119

124120
[build-system]
125121
build-backend = "uv_build"
126-
requires = ["uv_build>=0.8.17,<0.9.0"]
122+
requires = ["uv_build<=0.10.11"]
127123

128124
[dependency-groups]
129125
dev = [
130126
"prek>=0.3.6",
131127
"pytest-asyncio>=0.25.0",
132128
"pytest>=8.3.5",
133-
"rich>=14.0.0",
134-
"ruff>=0.15.0",
135-
"typer-slim>=0.21.1"
129+
"ruff==0.15.6"
136130
]

src/mega/__main__.py

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,152 @@
1-
from mega.cli import main
1+
from __future__ import annotations
2+
3+
import contextlib
4+
import json
5+
import logging
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING, Annotated
8+
9+
import yarl
10+
from cyclopts import App, Parameter
11+
12+
from mega import __version__, env
13+
from mega.api import LOG_HTTP_TRAFFIC
14+
from mega.client import MegaNzClient
15+
from mega.transfer_it import TransferItClient
16+
from mega.utils import Site, setup_logger
17+
18+
if TYPE_CHECKING:
19+
from collections.abc import AsyncGenerator
20+
21+
22+
logger = logging.getLogger("mega")
23+
CWD = Path.cwd()
24+
25+
app = App(
26+
help=(
27+
"CLI app for the [bold black]Mega.nz[/bold black] and [bold black]Transfer.it[/bold black].\n"
28+
f"Set [bold green]{env.EMAIL.name}[/bold green] and [bold green]{env.PASSWORD.name}[/bold green]\n"
29+
"enviroment variables to use them as credentials for Mega"
30+
),
31+
help_format="rich",
32+
version=__version__,
33+
)
34+
35+
36+
@app.meta.default
37+
def verbose(
38+
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
39+
verbose: Annotated[
40+
int,
41+
Parameter(
42+
name=["-v", "--verbose"],
43+
count=True,
44+
help="Increase verbosity (-v shows debug logs, -vv shows HTTP traffic)",
45+
),
46+
] = 0,
47+
) -> None:
48+
if verbose > 1:
49+
LOG_HTTP_TRAFFIC.set(True)
50+
51+
level = logging.DEBUG if verbose else logging.INFO
52+
setup_logger(level)
53+
app(tokens)
54+
55+
56+
@contextlib.asynccontextmanager
57+
async def connect() -> AsyncGenerator[MegaNzClient]:
58+
async with MegaNzClient() as mega:
59+
await mega.login(env.EMAIL, env.PASSWORD)
60+
with mega.progress_bar:
61+
yield mega
62+
63+
64+
async def transfer_it(url: str, output_dir: Path) -> None:
65+
async with TransferItClient() as client:
66+
with client.progress_bar:
67+
transfer_id = client.parse_url(url)
68+
logger.info(f"Downloading '{url}'")
69+
results = await client.download_transfer(transfer_id, output_dir)
70+
logger.info(
71+
f"Download of '{url}' finished. Successful = {len(results.success)}, failed = {len(results.fails)}"
72+
)
73+
74+
75+
@app.command()
76+
async def download(url: str, output_dir: Path = CWD) -> None:
77+
"""Download a public file or folder by its URL (transfer.it / mega.nz)"""
78+
79+
site = Site(yarl.URL(url).origin())
80+
if site is Site.TRANSFER_IT:
81+
return await transfer_it(url, output_dir)
82+
83+
async with connect() as mega:
84+
parsed_url = mega.parse_url(url)
85+
if parsed_url.is_folder:
86+
await download_folder(mega, url, output_dir)
87+
else:
88+
await download_file(mega, url, output_dir)
89+
90+
91+
@app.command()
92+
async def dump(output_dir: Path = CWD) -> None:
93+
"""Dump a copy of your filesystem to disk"""
94+
95+
async with connect() as mega:
96+
fs = await mega.get_filesystem()
97+
out = output_dir / "filesystem.json"
98+
out.parent.mkdir(exist_ok=True)
99+
logger.info(f"Creating filesystem dump at '{out!s}'")
100+
out.write_text(json.dumps(fs.dump(), indent=2, ensure_ascii=False))
101+
102+
103+
@app.command()
104+
async def stats() -> None:
105+
"""Show account stats"""
106+
107+
async with connect() as mega:
108+
stats = await mega.get_account_stats()
109+
logger.info(f"Account stats for {env.EMAIL or 'TEMP ACCOUNT'}:")
110+
logger.info(stats.storage.dump())
111+
logger.info(stats.balance.dump())
112+
fs = await mega.get_filesystem()
113+
metrics = {root.attributes.name: stats.metrics[root.id] for root in (fs.root, fs.inbox, fs.trash_bin)}
114+
logger.info(metrics)
115+
116+
117+
@app.command()
118+
async def upload(file_path: Path) -> None:
119+
"""Upload a file to your account"""
120+
121+
async with connect() as mega:
122+
if not env.EMAIL:
123+
logger.warning("Files uploaded by a temp account can not be exported")
124+
125+
folder = await mega.create_folder("uploaded by mega.py")
126+
logger.info(f'Uploading "{file_path!s}"')
127+
file = await mega.upload(file_path, folder.id)
128+
path = (await mega.get_filesystem()).absolute_path(file.id)
129+
logger.info(f'File uploaded to your cloud. Path = "{path}"')
130+
if not env.EMAIL:
131+
return
132+
133+
link = await mega.export(file)
134+
logger.info(f'Public link for "{file_path!s}": {link}')
135+
136+
137+
async def download_file(mega: MegaNzClient, url: str, output: Path) -> None:
138+
public_handle, public_key = mega.parse_file_url(url)
139+
logger.info(f"Downloading {url}")
140+
path = await mega.download_public_file(public_handle, public_key, output)
141+
logger.info(f'Download of {url} finished. File save at "{path!s}"')
142+
143+
144+
async def download_folder(mega: MegaNzClient, url: str, output: Path) -> None:
145+
public_handle, public_key, root_node = mega.parse_folder_url(url)
146+
logger.info(f"Downloading {url}")
147+
results = await mega.download_public_folder(public_handle, public_key, output, root_node)
148+
logger.info(f"Download of '{url}' finished. Successful = {len(results.success)}, failed = {len(results.fails)}")
149+
2150

3151
if __name__ == "__main__":
4-
main()
152+
app.meta()

0 commit comments

Comments
 (0)