async-mega-py 2.0.4.dev0__tar.gz → 2.1.0__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 (30) hide show
  1. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/PKG-INFO +42 -42
  2. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/README.md +37 -34
  3. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/pyproject.toml +9 -18
  4. async_mega_py-2.0.4.dev0/src/mega/cli/__init__.py → async_mega_py-2.1.0/src/mega/__main__.py +36 -23
  5. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/api.py +13 -10
  6. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/auth.py +46 -49
  7. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/chunker.py +39 -15
  8. async_mega_py-2.1.0/src/mega/client.py +290 -0
  9. async_mega_py-2.1.0/src/mega/core.py +450 -0
  10. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/crypto.py +16 -34
  11. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/data_structures.py +149 -49
  12. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/download.py +24 -12
  13. async_mega_py-2.1.0/src/mega/env.py +26 -0
  14. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/errors.py +13 -0
  15. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/filesystem.py +90 -7
  16. async_mega_py-2.0.4.dev0/src/mega/progress.py → async_mega_py-2.1.0/src/mega/progress/__init__.py +17 -75
  17. async_mega_py-2.1.0/src/mega/progress/rich_progress.py +58 -0
  18. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/transfer_it.py +19 -17
  19. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/upload.py +23 -24
  20. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/utils.py +63 -4
  21. async_mega_py-2.1.0/src/mega/vault.py +67 -0
  22. async_mega_py-2.0.4.dev0/src/mega/__main__.py +0 -4
  23. async_mega_py-2.0.4.dev0/src/mega/cli/app.py +0 -59
  24. async_mega_py-2.0.4.dev0/src/mega/client.py +0 -320
  25. async_mega_py-2.0.4.dev0/src/mega/core.py +0 -329
  26. async_mega_py-2.0.4.dev0/src/mega/env.py +0 -27
  27. async_mega_py-2.0.4.dev0/src/mega/vault.py +0 -110
  28. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/LICENSE +0 -0
  29. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/__init__.py +0 -0
  30. {async_mega_py-2.0.4.dev0 → async_mega_py-2.1.0}/src/mega/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: async-mega-py
3
- Version: 2.0.4.dev0
2
+ Name: async-mega.py
3
+ Version: 2.1.0
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
@@ -23,21 +23,18 @@ Classifier: Programming Language :: Python :: 3.15
23
23
  Classifier: Programming Language :: Python
24
24
  Requires-Dist: aiohttp>=3.11.18
25
25
  Requires-Dist: aiolimiter>=1.2.1
26
- Requires-Dist: pycryptodome==3.19.0
27
- Requires-Dist: async-mega-py[default] ; extra == 'cli'
28
- Requires-Dist: typer-slim>=0.21.1 ; extra == 'cli'
29
- Requires-Dist: python-dotenv>=1.2.1 ; extra == 'default'
30
- Requires-Dist: rich>=14.0.0 ; extra == 'default'
26
+ Requires-Dist: pycryptodome>=3.20.0
27
+ Requires-Dist: cyclopts>=4.10.1 ; extra == 'cli'
28
+ Requires-Dist: python-dotenv>=1.2.1 ; extra == 'cli'
31
29
  Requires-Python: >=3.11
32
30
  Project-URL: Homepage, https://github.com/NTFSvolume/mega.py
33
31
  Provides-Extra: cli
34
- Provides-Extra: default
35
32
  Description-Content-Type: text/markdown
36
33
 
37
34
  # Async Mega.py
38
35
 
39
36
  [![PyPI - Version](https://img.shields.io/pypi/v/async-mega-py)](https://pypi.org/project/async-mega-py/)
40
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/async-mega-py)](https://pypi.org/project/async-mega-py/)
37
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/async-mega-py?style=flat)](https://pypi.org/project/async-mega-py/)
41
38
  [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/NTFSvolume/mega.py/actions/workflows/ruff.yml)
42
39
  [![GitHub License](https://img.shields.io/github/license/NTFSvolume/mega.py)](https://github.com/NTFSvolume/mega.py/blob/master/LICENSE)
43
40
  [![CI](https://github.com/NTFSvolume/mega.py/actions/workflows/ci.yml/badge.svg)](https://github.com/NTFSvolume/mega.py/actions/workflows/ci.yml)
@@ -99,14 +96,10 @@ Login should always be the first thing you do. Almost all operations require a v
99
96
  ```python
100
97
  from mega.client import MegaNzClient
101
98
 
102
- async with MegaNzClient() as mega:
103
- await mega.login() # login using a temporary anonymous account
104
-
105
- # Also works without using it as a context manager,
99
+ # Also works without using it as a context manager, but you have the responsability to close the session at the end
106
100
  mega = MegaNzClient()
107
- await mega.login()
108
- # but you have the responsability to close the session
109
- # mega.close()
101
+ await mega.login() # login using a temporary anonymous account
102
+ # await mega.close()
110
103
 
111
104
  ```
112
105
 
@@ -133,19 +126,27 @@ contact = "test@mega.nz"
133
126
  await mega.add_contact(contact)
134
127
  await mega.remove_contact(contact)
135
128
 
129
+ async def view_paths():
130
+ fs = await mega.get_filesystem()
131
+ print (fs.paths.values())
132
+
136
133
  # Create a folder
137
134
  await mega.create_folder('new_folder')
138
135
  await mega.create_folder('new_folder/sub_folder/subsub_folder')
136
+ await view_paths()
139
137
 
140
138
  # Rename a file or a folder
141
139
  folder = await mega.find('new_folder/sub_folder/subsub_folder')
142
140
  await mega.rename(folder, new_name='my_new_name')
141
+ await view_paths()
143
142
 
144
- # Delete or destroy folder
145
-
146
- await mega.delete(folder.id) # Send this node to the trash bin (still counts towards your quota)
147
- await mega.destroy(folder.id) # This removes it completely from your account
143
+ # Send this node to the trash bin (still counts towards your quota)
144
+ await mega.delete(folder.id)
145
+ await view_paths()
148
146
 
147
+ # This removes it completely from your account
148
+ await mega.destroy(folder.id)
149
+ await view_paths()
149
150
  ```
150
151
 
151
152
  ### Upload / Downloads
@@ -154,29 +155,29 @@ await mega.destroy(folder.id) # This removes it completely from your account
154
155
  # Upload a file, and get its public link
155
156
  my_real_file = '/home/user/myfile.doc' # Change this to a real file path!
156
157
  uploaded_file = await mega.upload(my_real_file) # Upload returns the Node that represents the file you just uploaded
157
- await mega.export(uploaded_file)
158
+ await mega.export(uploaded_file) # This only works with real accounts. Temp accounts can't create public links
158
159
 
159
160
  # Download a file from your account
160
161
  output_dir = "my downloads"
161
162
  await mega.download(uploaded_file, output_dir)
162
163
 
163
164
  # Download a public file
164
- url = "https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps"
165
- public_handle, public_key = mega.parse_file_url(url)
165
+ file_url = "https://mega.nz/file/vJ9EBJBC#vA1XpbngXcnN7lqXEWGQFPDc5ZlG6yltCHwGN-0O2LQ"
166
+ public_handle, public_key = mega.parse_file_url(file_url)
166
167
  await mega.download_public_file(public_handle, public_key, output_dir)
167
168
 
168
169
  # Download a public folder
169
- url = "https://mega.co.nz/#F!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc"
170
- public_handle, public_key, selected_node = mega.parse_folder_url(url)
171
- success, fails = await mega.download_public_folder(public_handle, public_key, output_dir, selected_node)
172
- print(f"Download of '{url!s}' finished. Successful downloads {len(success)}, failed {len(fails)}")
170
+ folder_url = "https://mega.nz/folder/CJ8y1SDJ#KX8YfTd526P4o_hDH02jLQ"
171
+ public_handle, public_key, selected_node = mega.parse_folder_url(folder_url)
172
+ results = await mega.download_public_folder(public_handle, public_key, output_dir, selected_node)
173
+ for node_id, result in results.items():
174
+ print ((node_id, result))
173
175
 
174
176
  # Import a file from URL
175
- url = "https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps"
176
- public_handle, public_key = mega.parse_file_url(url)
177
+ public_handle, public_key = mega.parse_file_url(file_url)
177
178
  await mega.import_public_file(public_handle, public_key, dest_node_id=folder.id)
178
179
 
179
- # How do you know if an URL is a file or folder? call the more generic parse_url method
180
+ # How do you know if an URL is a file or folder?
180
181
  result = mega.parse_url(url)
181
182
  print (result.is_folder)
182
183
  ```
@@ -185,10 +186,8 @@ print (result.is_folder)
185
186
  > You can show a progress bar on the terminal for each download/upload by calling them within the `progress_bar` context manager (needs optional dependency `rich` to be installed):
186
187
 
187
188
  ```python
188
- url = "https://mega.co.nz/#F!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc"
189
- public_handle, public_key = mega.parse_folder_url(url)
190
189
  with mega.progress_bar:
191
- success, fails = await mega.download_public_folder(public_handle, public_key, output_dir)
190
+ results = await mega.download_public_folder(public_handle, public_key, output_dir / "with_bar")
192
191
  ```
193
192
 
194
193
  ## The filesystem object
@@ -277,12 +276,11 @@ The output will be:
277
276
 
278
277
  ```python
279
278
  # Get the path to a node
280
- path = fs.absolute_path("0fPFklV3")
281
- # Should be: /tests/scripts/notes.txt
279
+ fs.absolute_path("0fPFklV3") # /tests/scripts/notes.txt
282
280
 
283
281
  # Find a node by its *exact* path
284
282
  result = fs.find("/tests/scripts/notes.txt")
285
- assert result.id == "0fPFklV3"
283
+ print (result.id) # "0fPFklV3"
286
284
 
287
285
  ```
288
286
 
@@ -293,7 +291,7 @@ list(fs.deleted)
293
291
  # List all the children of a folder (resursive)
294
292
  folder = fs.find("/tests")
295
293
  for node in fs.iterdir(folder.id, recursive=True):
296
- print(fs.absolute_path(node))
294
+ print(fs.absolute_path(node.id))
297
295
 
298
296
  ```
299
297
  Output will be:
@@ -306,7 +304,9 @@ Output will be:
306
304
  "/tests/scripts",
307
305
  "/tests/scripts/notes.txt",
308
306
  "/tests/scripts/script.js",
309
- "/tests/scripts/styles.css"
307
+ "/tests/scripts/styles.css",
308
+ "/tests/setup.sh",
309
+ "/tests/utils.py",
310
310
  ]
311
311
  ```
312
312
 
@@ -324,7 +324,7 @@ dict(fs.search("/tests/logo.png"))
324
324
  ## Transfer.it
325
325
 
326
326
  > [!NOTE]
327
- > The `transfer.it` client does not support uploads (yet!)
327
+ > The `transfer.it` client does not support uploads yet (PRs welcome)
328
328
 
329
329
  ```python
330
330
  from mega.transfer_it import TransferItClient
@@ -335,8 +335,8 @@ async with TransferItClient() as client:
335
335
  # but it does not have root, inbox or trash_bin nodes
336
336
  fs = await client.get_filesystem(transfer_id)
337
337
  output_dir = "My downloads"
338
- success, fails = await client.download_transfer(transfer_id, output_dir)
339
- logger.info(f"Download of '{url!s}' finished. Successful downloads {len(success)}, failed {len(fails)}")
338
+ results = await client.download_transfer(transfer_id, output_dir)
339
+ print (results)
340
340
  ```
341
341
 
342
342
  ## CLI
@@ -371,6 +371,6 @@ Usage: mega-py [OPTIONS] COMMAND [ARGS]...
371
371
  ```
372
372
 
373
373
  > [!TIP]
374
- > The CLI app does *not* accept login credentials, but you can still use your account by setting up the `MEGA_NZ_EMAIL` and `MEGA_NZ_PASSWORD` enviroment variables
374
+ > The CLI app does *not* accept login credentials, but you can still use your account by setting up the `MEGA_EMAIL` and `MEGA_PWD` enviroment variables
375
375
  >
376
376
  > It will also read them from an `.env` file (if found)
@@ -1,7 +1,7 @@
1
1
  # Async Mega.py
2
2
 
3
3
  [![PyPI - Version](https://img.shields.io/pypi/v/async-mega-py)](https://pypi.org/project/async-mega-py/)
4
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/async-mega-py)](https://pypi.org/project/async-mega-py/)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/async-mega-py?style=flat)](https://pypi.org/project/async-mega-py/)
5
5
  [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/NTFSvolume/mega.py/actions/workflows/ruff.yml)
6
6
  [![GitHub License](https://img.shields.io/github/license/NTFSvolume/mega.py)](https://github.com/NTFSvolume/mega.py/blob/master/LICENSE)
7
7
  [![CI](https://github.com/NTFSvolume/mega.py/actions/workflows/ci.yml/badge.svg)](https://github.com/NTFSvolume/mega.py/actions/workflows/ci.yml)
@@ -63,14 +63,10 @@ Login should always be the first thing you do. Almost all operations require a v
63
63
  ```python
64
64
  from mega.client import MegaNzClient
65
65
 
66
- async with MegaNzClient() as mega:
67
- await mega.login() # login using a temporary anonymous account
68
-
69
- # Also works without using it as a context manager,
66
+ # Also works without using it as a context manager, but you have the responsability to close the session at the end
70
67
  mega = MegaNzClient()
71
- await mega.login()
72
- # but you have the responsability to close the session
73
- # mega.close()
68
+ await mega.login() # login using a temporary anonymous account
69
+ # await mega.close()
74
70
 
75
71
  ```
76
72
 
@@ -97,19 +93,27 @@ contact = "test@mega.nz"
97
93
  await mega.add_contact(contact)
98
94
  await mega.remove_contact(contact)
99
95
 
96
+ async def view_paths():
97
+ fs = await mega.get_filesystem()
98
+ print (fs.paths.values())
99
+
100
100
  # Create a folder
101
101
  await mega.create_folder('new_folder')
102
102
  await mega.create_folder('new_folder/sub_folder/subsub_folder')
103
+ await view_paths()
103
104
 
104
105
  # Rename a file or a folder
105
106
  folder = await mega.find('new_folder/sub_folder/subsub_folder')
106
107
  await mega.rename(folder, new_name='my_new_name')
108
+ await view_paths()
107
109
 
108
- # Delete or destroy folder
109
-
110
- await mega.delete(folder.id) # Send this node to the trash bin (still counts towards your quota)
111
- await mega.destroy(folder.id) # This removes it completely from your account
110
+ # Send this node to the trash bin (still counts towards your quota)
111
+ await mega.delete(folder.id)
112
+ await view_paths()
112
113
 
114
+ # This removes it completely from your account
115
+ await mega.destroy(folder.id)
116
+ await view_paths()
113
117
  ```
114
118
 
115
119
  ### Upload / Downloads
@@ -118,29 +122,29 @@ await mega.destroy(folder.id) # This removes it completely from your account
118
122
  # Upload a file, and get its public link
119
123
  my_real_file = '/home/user/myfile.doc' # Change this to a real file path!
120
124
  uploaded_file = await mega.upload(my_real_file) # Upload returns the Node that represents the file you just uploaded
121
- await mega.export(uploaded_file)
125
+ await mega.export(uploaded_file) # This only works with real accounts. Temp accounts can't create public links
122
126
 
123
127
  # Download a file from your account
124
128
  output_dir = "my downloads"
125
129
  await mega.download(uploaded_file, output_dir)
126
130
 
127
131
  # Download a public file
128
- url = "https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps"
129
- public_handle, public_key = mega.parse_file_url(url)
132
+ file_url = "https://mega.nz/file/vJ9EBJBC#vA1XpbngXcnN7lqXEWGQFPDc5ZlG6yltCHwGN-0O2LQ"
133
+ public_handle, public_key = mega.parse_file_url(file_url)
130
134
  await mega.download_public_file(public_handle, public_key, output_dir)
131
135
 
132
136
  # Download a public folder
133
- url = "https://mega.co.nz/#F!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc"
134
- public_handle, public_key, selected_node = mega.parse_folder_url(url)
135
- success, fails = await mega.download_public_folder(public_handle, public_key, output_dir, selected_node)
136
- print(f"Download of '{url!s}' finished. Successful downloads {len(success)}, failed {len(fails)}")
137
+ folder_url = "https://mega.nz/folder/CJ8y1SDJ#KX8YfTd526P4o_hDH02jLQ"
138
+ public_handle, public_key, selected_node = mega.parse_folder_url(folder_url)
139
+ results = await mega.download_public_folder(public_handle, public_key, output_dir, selected_node)
140
+ for node_id, result in results.items():
141
+ print ((node_id, result))
137
142
 
138
143
  # Import a file from URL
139
- url = "https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps"
140
- public_handle, public_key = mega.parse_file_url(url)
144
+ public_handle, public_key = mega.parse_file_url(file_url)
141
145
  await mega.import_public_file(public_handle, public_key, dest_node_id=folder.id)
142
146
 
143
- # How do you know if an URL is a file or folder? call the more generic parse_url method
147
+ # How do you know if an URL is a file or folder?
144
148
  result = mega.parse_url(url)
145
149
  print (result.is_folder)
146
150
  ```
@@ -149,10 +153,8 @@ print (result.is_folder)
149
153
  > You can show a progress bar on the terminal for each download/upload by calling them within the `progress_bar` context manager (needs optional dependency `rich` to be installed):
150
154
 
151
155
  ```python
152
- url = "https://mega.co.nz/#F!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc"
153
- public_handle, public_key = mega.parse_folder_url(url)
154
156
  with mega.progress_bar:
155
- success, fails = await mega.download_public_folder(public_handle, public_key, output_dir)
157
+ results = await mega.download_public_folder(public_handle, public_key, output_dir / "with_bar")
156
158
  ```
157
159
 
158
160
  ## The filesystem object
@@ -241,12 +243,11 @@ The output will be:
241
243
 
242
244
  ```python
243
245
  # Get the path to a node
244
- path = fs.absolute_path("0fPFklV3")
245
- # Should be: /tests/scripts/notes.txt
246
+ fs.absolute_path("0fPFklV3") # /tests/scripts/notes.txt
246
247
 
247
248
  # Find a node by its *exact* path
248
249
  result = fs.find("/tests/scripts/notes.txt")
249
- assert result.id == "0fPFklV3"
250
+ print (result.id) # "0fPFklV3"
250
251
 
251
252
  ```
252
253
 
@@ -257,7 +258,7 @@ list(fs.deleted)
257
258
  # List all the children of a folder (resursive)
258
259
  folder = fs.find("/tests")
259
260
  for node in fs.iterdir(folder.id, recursive=True):
260
- print(fs.absolute_path(node))
261
+ print(fs.absolute_path(node.id))
261
262
 
262
263
  ```
263
264
  Output will be:
@@ -270,7 +271,9 @@ Output will be:
270
271
  "/tests/scripts",
271
272
  "/tests/scripts/notes.txt",
272
273
  "/tests/scripts/script.js",
273
- "/tests/scripts/styles.css"
274
+ "/tests/scripts/styles.css",
275
+ "/tests/setup.sh",
276
+ "/tests/utils.py",
274
277
  ]
275
278
  ```
276
279
 
@@ -288,7 +291,7 @@ dict(fs.search("/tests/logo.png"))
288
291
  ## Transfer.it
289
292
 
290
293
  > [!NOTE]
291
- > The `transfer.it` client does not support uploads (yet!)
294
+ > The `transfer.it` client does not support uploads yet (PRs welcome)
292
295
 
293
296
  ```python
294
297
  from mega.transfer_it import TransferItClient
@@ -299,8 +302,8 @@ async with TransferItClient() as client:
299
302
  # but it does not have root, inbox or trash_bin nodes
300
303
  fs = await client.get_filesystem(transfer_id)
301
304
  output_dir = "My downloads"
302
- success, fails = await client.download_transfer(transfer_id, output_dir)
303
- logger.info(f"Download of '{url!s}' finished. Successful downloads {len(success)}, failed {len(fails)}")
305
+ results = await client.download_transfer(transfer_id, output_dir)
306
+ print (results)
304
307
  ```
305
308
 
306
309
  ## CLI
@@ -335,6 +338,6 @@ Usage: mega-py [OPTIONS] COMMAND [ARGS]...
335
338
  ```
336
339
 
337
340
  > [!TIP]
338
- > The CLI app does *not* accept login credentials, but you can still use your account by setting up the `MEGA_NZ_EMAIL` and `MEGA_NZ_PASSWORD` enviroment variables
341
+ > The CLI app does *not* accept login credentials, but you can still use your account by setting up the `MEGA_EMAIL` and `MEGA_PWD` enviroment variables
339
342
  >
340
343
  > It will also read them from an `.env` file (if found)
@@ -20,7 +20,7 @@ classifiers = [
20
20
  dependencies = [
21
21
  "aiohttp>=3.11.18",
22
22
  "aiolimiter>=1.2.1",
23
- "pycryptodome==3.19.0"
23
+ "pycryptodome>=3.20.0"
24
24
  ]
25
25
  description = "Python library for the Mega.nz and Transfer.it API"
26
26
  keywords = [
@@ -34,21 +34,17 @@ license = "Apache-2.0"
34
34
  license-files = ["LICENSE"]
35
35
  readme = "README.md"
36
36
  requires-python = ">=3.11"
37
- version = "2.0.4.dev"
37
+ version = "2.1.0"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  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"
47
43
  ]
48
44
 
49
45
  [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"
52
48
 
53
49
  [project.urls]
54
50
  Homepage = "https://github.com/NTFSvolume/mega.py"
@@ -123,17 +119,12 @@ module-name = "mega"
123
119
 
124
120
  [build-system]
125
121
  build-backend = "uv_build"
126
- requires = ["uv_build>=0.8.17,<0.9.0"]
122
+ requires = ["uv_build<=0.10.11"]
127
123
 
128
124
  [dependency-groups]
129
125
  dev = [
130
- "coverage[toml]>=7.7.0",
131
- "pre-commit>=4.5.0",
126
+ "prek>=0.3.6",
132
127
  "pytest-asyncio>=0.25.0",
133
- "pytest-cov>=6.0.0",
134
128
  "pytest>=8.3.5",
135
- "requests-mock>=1.12.1",
136
- "rich>=14.0.0",
137
- "ruff>=0.13.1",
138
- "typing-extensions>=4.15.0"
129
+ "ruff==0.15.6"
139
130
  ]
@@ -6,12 +6,11 @@ import logging
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Annotated
8
8
 
9
- import typer
10
9
  import yarl
10
+ from cyclopts import App, Parameter
11
11
 
12
- from mega import __version__, env, progress
12
+ from mega import __version__, env
13
13
  from mega.api import LOG_HTTP_TRAFFIC
14
- from mega.cli.app import CLIApp
15
14
  from mega.client import MegaNzClient
16
15
  from mega.transfer_it import TransferItClient
17
16
  from mega.utils import Site, setup_logger
@@ -21,14 +20,26 @@ if TYPE_CHECKING:
21
20
 
22
21
 
23
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
+ )
24
34
 
25
35
 
36
+ @app.meta.default
26
37
  def verbose(
38
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
27
39
  verbose: Annotated[
28
40
  int,
29
- typer.Option(
30
- "-v",
31
- "--verbose",
41
+ Parameter(
42
+ name=["-v", "--verbose"],
32
43
  count=True,
33
44
  help="Increase verbosity (-v shows debug logs, -vv shows HTTP traffic)",
34
45
  ),
@@ -39,18 +50,7 @@ def verbose(
39
50
 
40
51
  level = logging.DEBUG if verbose else logging.INFO
41
52
  setup_logger(level)
42
-
43
-
44
- app = CLIApp(
45
- add_completion=False,
46
- callback=verbose,
47
- help=(
48
- "CLI app for the [bold black]Mega.nz[/bold black] and [bold black]Transfer.it[/bold black].\n"
49
- f"Set [bold green]{env.NAMES.EMAIL}[/bold green] and [bold green]{env.NAMES.PASSWORD}[/bold green] enviroment variables to use them as credentials for Mega"
50
- ),
51
- epilog=f"v{__version__}",
52
- )
53
- CWD = Path.cwd()
53
+ app(tokens)
54
54
 
55
55
 
56
56
  @contextlib.asynccontextmanager
@@ -63,7 +63,7 @@ async def connect() -> AsyncGenerator[MegaNzClient]:
63
63
 
64
64
  async def transfer_it(url: str, output_dir: Path) -> None:
65
65
  async with TransferItClient() as client:
66
- with progress.new_progress():
66
+ with client.progress_bar:
67
67
  transfer_id = client.parse_url(url)
68
68
  logger.info(f"Downloading '{url}'")
69
69
  results = await client.download_transfer(transfer_id, output_dir)
@@ -75,6 +75,7 @@ async def transfer_it(url: str, output_dir: Path) -> None:
75
75
  @app.command()
76
76
  async def download(url: str, output_dir: Path = CWD) -> None:
77
77
  """Download a public file or folder by its URL (transfer.it / mega.nz)"""
78
+
78
79
  site = Site(yarl.URL(url).origin())
79
80
  if site is Site.TRANSFER_IT:
80
81
  return await transfer_it(url, output_dir)
@@ -82,13 +83,15 @@ async def download(url: str, output_dir: Path = CWD) -> None:
82
83
  async with connect() as mega:
83
84
  parsed_url = mega.parse_url(url)
84
85
  if parsed_url.is_folder:
85
- return await download_folder(mega, url, output_dir)
86
- await download_file(mega, url, output_dir)
86
+ await download_folder(mega, url, output_dir)
87
+ else:
88
+ await download_file(mega, url, output_dir)
87
89
 
88
90
 
89
91
  @app.command()
90
92
  async def dump(output_dir: Path = CWD) -> None:
91
93
  """Dump a copy of your filesystem to disk"""
94
+
92
95
  async with connect() as mega:
93
96
  fs = await mega.get_filesystem()
94
97
  out = output_dir / "filesystem.json"
@@ -100,6 +103,7 @@ async def dump(output_dir: Path = CWD) -> None:
100
103
  @app.command()
101
104
  async def stats() -> None:
102
105
  """Show account stats"""
106
+
103
107
  async with connect() as mega:
104
108
  stats = await mega.get_account_stats()
105
109
  logger.info(f"Account stats for {env.EMAIL or 'TEMP ACCOUNT'}:")
@@ -113,10 +117,19 @@ async def stats() -> None:
113
117
  @app.command()
114
118
  async def upload(file_path: Path) -> None:
115
119
  """Upload a file to your account"""
120
+
116
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
+
117
125
  folder = await mega.create_folder("uploaded by mega.py")
118
126
  logger.info(f'Uploading "{file_path!s}"')
119
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
+
120
133
  link = await mega.export(file)
121
134
  logger.info(f'Public link for "{file_path!s}": {link}')
122
135
 
@@ -135,5 +148,5 @@ async def download_folder(mega: MegaNzClient, url: str, output: Path) -> None:
135
148
  logger.info(f"Download of '{url}' finished. Successful = {len(results.success)}, failed = {len(results.fails)}")
136
149
 
137
150
 
138
- def main() -> None:
139
- app()
151
+ if __name__ == "__main__":
152
+ app.meta()
@@ -14,7 +14,7 @@ import aiohttp
14
14
  import yarl
15
15
  from aiolimiter import AsyncLimiter
16
16
 
17
- from mega.crypto import generate_hashcash_token
17
+ from mega.crypto import generate_hashcash
18
18
  from mega.errors import RequestError, RetryRequestError
19
19
  from mega.utils import random_id, random_u32int
20
20
 
@@ -38,10 +38,10 @@ def retry(
38
38
  exceptions: Sequence[type[Exception]] | type[Exception],
39
39
  attempts: int = 10,
40
40
  delay: float = 0.5,
41
- min_delay: float = 2.0,
42
41
  max_delay: float = 30.0,
43
42
  backoff: int = 2,
44
43
  ) -> Callable[[Callable[_P, Coroutine[None, None, _R]]], Callable[_P, Coroutine[None, None, _R]]]:
44
+ min_delay: float = 2.0
45
45
  if not isinstance(exceptions, Sequence):
46
46
  exceptions = [exceptions]
47
47
 
@@ -86,7 +86,6 @@ class MegaAPI:
86
86
  self.__session = session
87
87
  self._auto_close_session = session is None
88
88
  self._rate_limiter = AsyncLimiter(100, 60)
89
- logger.disabled = not LOG_HTTP_TRAFFIC.get()
90
89
 
91
90
  @property
92
91
  def entrypoint(self) -> yarl.URL:
@@ -148,7 +147,7 @@ class MegaAPI:
148
147
  msg = f"Login failed. Mega requested a proof of work with xhashcash: {xhashcash_challenge}"
149
148
  raise RequestError(msg)
150
149
 
151
- headers["X-Hashcash"] = await asyncio.to_thread(generate_hashcash_token, xhashcash_challenge)
150
+ headers["X-Hashcash"] = await asyncio.to_thread(generate_hashcash, xhashcash_challenge)
152
151
  continue
153
152
 
154
153
  return await self._parse_response(resp)
@@ -176,8 +175,9 @@ class MegaAPI:
176
175
  **kwargs: Any,
177
176
  ) -> AsyncGenerator[aiohttp.ClientResponse]:
178
177
  kwargs["headers"] = _DEFAULT_HEADERS | (headers or {})
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}")
178
+ 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
181
  async with self._rate_limiter, self._session.request(method, url, **kwargs) as resp:
182
182
  yield resp
183
183
 
@@ -185,7 +185,8 @@ class MegaAPI:
185
185
  async def _parse_response(response: aiohttp.ClientResponse) -> Any:
186
186
  json_resp = await response.json()
187
187
  resp = json_resp
188
- logger.debug(f"Got response [{response.status}] json={json_resp!r}")
188
+ if LOG_HTTP_TRAFFIC.get():
189
+ logger.debug(f"Got response [{response.status}] json={json_resp!r}")
189
190
 
190
191
  if isinstance(json_resp, list) and len(json_resp) == 1:
191
192
  resp = json_resp[0]
@@ -197,13 +198,13 @@ class MegaAPI:
197
198
  if resp == -3:
198
199
  msg = "Request failed, retrying"
199
200
  logger.warning(msg)
200
- raise RetryRequestError()
201
+ raise RetryRequestError
201
202
  raise RequestError(resp)
202
203
 
203
204
  return resp
204
205
 
205
206
 
206
- class AbstractApiClient:
207
+ class APIContextManager:
207
208
  __slots__ = ("_api",)
208
209
 
209
210
  def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
@@ -215,5 +216,7 @@ class AbstractApiClient:
215
216
  async def __aexit__(self, *_) -> None:
216
217
  await self.close()
217
218
 
218
- async def close(self) -> None:
219
+ async def aclose(self) -> None:
219
220
  await self._api.close()
221
+
222
+ close = aclose