async-mega-py 2.3.0__tar.gz → 2.4.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 (24) hide show
  1. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/PKG-INFO +1 -1
  2. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/pyproject.toml +1 -1
  3. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/data_structures.py +4 -6
  4. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/filesystem.py +47 -113
  5. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/vault.py +9 -4
  6. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/LICENSE +0 -0
  7. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/README.md +0 -0
  8. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/__init__.py +0 -0
  9. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/__main__.py +0 -0
  10. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/api.py +0 -0
  11. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/auth.py +0 -0
  12. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/chunker.py +0 -0
  13. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/client.py +0 -0
  14. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/core.py +0 -0
  15. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/crypto.py +0 -0
  16. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/download.py +0 -0
  17. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/env.py +0 -0
  18. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/errors.py +0 -0
  19. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/progress/__init__.py +0 -0
  20. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/progress/rich_progress.py +0 -0
  21. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/py.typed +0 -0
  22. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/transfer_it.py +0 -0
  23. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/upload.py +0 -0
  24. {async_mega_py-2.3.0 → async_mega_py-2.4.0}/src/mega/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-mega.py
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -34,7 +34,7 @@ license = "Apache-2.0"
34
34
  license-files = ["LICENSE"]
35
35
  readme = "README.md"
36
36
  requires-python = ">=3.11"
37
- version = "2.3.0"
37
+ version = "2.4.0"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  cli = [
@@ -29,7 +29,6 @@ SharedKeys: TypeAlias = dict[NodeID, tuple[int, ...]]
29
29
 
30
30
  NodeKey = tuple[int, int, int, int]
31
31
  ComposedFileKey = tuple[int, int, int, int, int, int, int, int]
32
- ComposedFolderKey = tuple[int, int, int, int]
33
32
 
34
33
 
35
34
  class ByteSize(int):
@@ -182,7 +181,7 @@ class Crypto(_DictDumper):
182
181
  iv: tuple[int, int]
183
182
  meta_mac: tuple[int, int]
184
183
 
185
- full_key: ComposedFileKey | ComposedFolderKey
184
+ full_key: ComposedFileKey | NodeKey
186
185
  share_key: NodeKey | None
187
186
 
188
187
  @classmethod
@@ -206,19 +205,18 @@ class Crypto(_DictDumper):
206
205
  raise RuntimeError(f"Invalid key, expected key len for files is 8, got {len(composed_key) = }")
207
206
 
208
207
  else:
209
- if len(key) != 4:
210
- raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(key) = }")
211
-
212
208
  iv = ()
213
209
  meta_mac = ()
214
210
  composed_key = key
211
+ if len(composed_key) != 4:
212
+ raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(composed_key) = }")
215
213
 
216
214
  return Crypto(key, iv, meta_mac, composed_key, None) # pyright: ignore[reportArgumentType]
217
215
 
218
216
  @classmethod
219
217
  def decompose(
220
218
  cls,
221
- composed_key: ComposedFileKey | ComposedFolderKey,
219
+ composed_key: ComposedFileKey | NodeKey,
222
220
  node_type: NodeType = NodeType.FILE,
223
221
  share_key: NodeKey | None = None,
224
222
  ) -> Crypto:
@@ -12,7 +12,7 @@ from types import MappingProxyType
12
12
  from typing import TYPE_CHECKING, Any, Final, NamedTuple, Self
13
13
 
14
14
  from mega.data_structures import Node, NodeID, NodeType, _DictDumper
15
- from mega.errors import MultipleNodesFoundError
15
+ from mega.errors import MultipleNodesFoundError, ValidationError
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Generator, Iterable, Iterator
@@ -48,12 +48,15 @@ def _resolve_paths(walker: _NodeWalker, *roots: Node) -> Generator[_NodeLookup]:
48
48
 
49
49
  @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True)
50
50
  class _NodeWalker:
51
- _nodes: MappingProxyType[NodeID, Node] = dataclasses.field(repr=False)
52
- _children: MappingProxyType[NodeID, tuple[NodeID, ...]] = dataclasses.field(repr=False)
51
+ nodes: MappingProxyType[NodeID, Node] = dataclasses.field(repr=False)
52
+ "A mapping of every node"
53
+
54
+ children: MappingProxyType[NodeID, tuple[NodeID, ...]] = dataclasses.field(repr=False)
55
+ "A mapping of nodes to their inmediate children"
53
56
 
54
57
  def _ls(self, node_id: NodeID, *, recursive: bool) -> Iterable[NodeID]:
55
58
  """Get ID of every child of this node"""
56
- for child_id in self._children.get(node_id, ()):
59
+ for child_id in self.children.get(node_id, ()):
57
60
  yield child_id
58
61
  if recursive:
59
62
  yield from self._ls(child_id, recursive=recursive)
@@ -61,14 +64,14 @@ class _NodeWalker:
61
64
  def iterdir(self, node_id: NodeID, *, recursive: bool = False) -> Iterable[Node]:
62
65
  """Iterate over the children in this node"""
63
66
  for child_id in self._ls(node_id, recursive=recursive):
64
- yield self._nodes[child_id]
67
+ yield self.nodes[child_id]
65
68
 
66
69
  def listdir(self, node_id: NodeID) -> list[Node]:
67
70
  """Get a list of children of this node (non recursive)"""
68
71
  return list(self.iterdir(node_id))
69
72
 
70
73
 
71
- @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True)
74
+ @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True, repr=False)
72
75
  class SimpleFileSystem(_NodeWalker, _DictDumper):
73
76
  """A simple representation of Mega.nz's file system.
74
77
 
@@ -82,6 +85,16 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
82
85
  file_count: int
83
86
  folder_count: int
84
87
 
88
+ def __repr__(self) -> str:
89
+ def fields():
90
+ for name, node in [("root", self.root), ("inbox", self.inbox), ("trash_bin", self.trash_bin)]:
91
+ yield name, node.id if node is not None else None
92
+ yield "files", self.file_count
93
+ yield "folders", self.folder_count
94
+
95
+ all_fields = ", ".join(f"{name}={value!r}" for name, value in fields())
96
+ return f"<{type(self).__name__}({all_fields})>"
97
+
85
98
  def __len__(self) -> int:
86
99
  return len(self.nodes)
87
100
 
@@ -95,16 +108,6 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
95
108
  """Get the node with this ID"""
96
109
  return self.nodes[node_id]
97
110
 
98
- @property
99
- def nodes(self) -> MappingProxyType[NodeID, Node]:
100
- """A mapping of every node"""
101
- return self._nodes
102
-
103
- @property
104
- def children(self) -> MappingProxyType[NodeID, tuple[NodeID, ...]]:
105
- """A mapping of nodes to their inmediate children"""
106
- return self._children
107
-
108
111
  @property
109
112
  def deleted(self) -> Iterable[Node]:
110
113
  """Files or folders currently on the trash bin (Non recursive)"""
@@ -119,6 +122,11 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
119
122
  nodes_map: dict[NodeID, Node] = {}
120
123
  children: dict[NodeID, list[NodeID]] = {}
121
124
 
125
+ def sanity_check(current: Node | None, found: Node) -> None:
126
+ if current is not None:
127
+ ids = current.id, found.id
128
+ raise ValidationError(f"Multiple nodes of type {found.type.name} found: {ids}")
129
+
122
130
  for node in nodes:
123
131
  nodes_map[node.id] = node
124
132
  if node.parent_id:
@@ -129,10 +137,13 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
129
137
  case NodeType.FOLDER:
130
138
  folder_count += 1
131
139
  case NodeType.ROOT_FOLDER:
140
+ sanity_check(root, node)
132
141
  root = node
133
142
  case NodeType.INBOX:
143
+ sanity_check(inbox, node)
134
144
  inbox = node
135
145
  case NodeType.TRASH:
146
+ sanity_check(trash_bin, node)
136
147
  trash_bin = node
137
148
  case _:
138
149
  raise RuntimeError # pyright: ignore[reportUnreachable]
@@ -143,8 +154,8 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
143
154
  trash_bin=trash_bin,
144
155
  file_count=file_count,
145
156
  folder_count=folder_count,
146
- _nodes=MappingProxyType(nodes_map),
147
- _children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}),
157
+ nodes=MappingProxyType(nodes_map),
158
+ children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}),
148
159
  )
149
160
 
150
161
  def dump(self) -> dict[str, Any]:
@@ -163,7 +174,7 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
163
174
  return cls.build(Node.from_dump(node) for node in dump["nodes"].values())
164
175
 
165
176
 
166
- @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True)
177
+ @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True, repr=False)
167
178
  class FileSystem(SimpleFileSystem):
168
179
  """Mega.nz's file system.
169
180
 
@@ -173,9 +184,13 @@ class FileSystem(SimpleFileSystem):
173
184
  NOTE: Mega's filesystem is **not POSIX-compliant**: multiple nodes may have the same path
174
185
  """
175
186
 
176
- _paths: MappingProxyType[NodeID, PurePosixPath] = dataclasses.field(repr=False)
177
- _inv_paths: MappingProxyType[PurePosixPath, tuple[NodeID, ...]] = dataclasses.field(repr=False)
178
- _deleted: frozenset[NodeID] = dataclasses.field(repr=False)
187
+ paths: MappingProxyType[NodeID, PurePosixPath]
188
+ """A mapping of every node to its absolute path within the filesystem"""
189
+
190
+ inv_paths: MappingProxyType[PurePosixPath, tuple[NodeID, ...]]
191
+ """A mapping of paths to every node located at that path"""
192
+
193
+ _deleted: frozenset[NodeID]
179
194
 
180
195
  @classmethod
181
196
  def build(cls, nodes: Iterable[Node]) -> Self:
@@ -209,26 +224,13 @@ class FileSystem(SimpleFileSystem):
209
224
  trash_bin=self.trash_bin,
210
225
  file_count=self.file_count,
211
226
  folder_count=self.folder_count,
212
- _nodes=self.nodes,
213
- _children=self.children,
214
- _paths=MappingProxyType(paths),
215
- _inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}),
227
+ nodes=self.nodes,
228
+ children=self.children,
229
+ paths=MappingProxyType(paths),
230
+ inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}),
216
231
  _deleted=frozenset(deleted_ids),
217
232
  )
218
233
 
219
- @property
220
- def paths(self) -> MappingProxyType[NodeID, PurePosixPath]:
221
- """A mapping of every node to its absolute path within the filesystem"""
222
- return self._paths
223
-
224
- @property
225
- def inv_paths(self) -> MappingProxyType[PurePosixPath, tuple[NodeID, ...]]:
226
- """A mapping of paths to every node located at that path
227
-
228
- Mega's filesystem is **not POSIX-compliant**: multiple nodes may have the same path
229
- """
230
- return self._inv_paths
231
-
232
234
  @property
233
235
  def files(self) -> Iterable[Node]:
234
236
  """All files that are NOT deleted (recursive)"""
@@ -260,7 +262,7 @@ class FileSystem(SimpleFileSystem):
260
262
 
261
263
  def absolute_path(self, node_id: NodeID) -> PurePosixPath:
262
264
  """Get the absolute path of this node"""
263
- return self._paths[node_id]
265
+ return self.paths[node_id]
264
266
 
265
267
  def search(
266
268
  self,
@@ -271,7 +273,7 @@ class FileSystem(SimpleFileSystem):
271
273
  """Returns nodes that have "query" as a substring on their path"""
272
274
  query = PurePosixPath(query).as_posix()
273
275
 
274
- for node_id, path in self._paths.items():
276
+ for node_id, path in self.paths.items():
275
277
  if query not in path.as_posix():
276
278
  continue
277
279
 
@@ -292,7 +294,7 @@ class FileSystem(SimpleFileSystem):
292
294
  """
293
295
  path = POSIX_ROOT / PurePosixPath(path)
294
296
  try:
295
- nodes = self._inv_paths[path]
297
+ nodes = self.inv_paths[path]
296
298
  except LookupError:
297
299
  msg = f'A node with path "{path!s}" does not exists'
298
300
  raise FileNotFoundError(errno.ENOENT, msg) from None
@@ -332,84 +334,18 @@ class FileSystem(SimpleFileSystem):
332
334
 
333
335
  return dump | dict( # noqa: C408
334
336
  deleted=sorted(self._deleted),
335
- paths={node_id: str(path) for node_id, path in self._paths.items()},
336
- inv_paths={str(path): node_id for path, node_id in self._inv_paths.items()},
337
+ paths={node_id: str(path) for node_id, path in self.paths.items()},
338
+ inv_paths={str(path): node_id for path, node_id in self.inv_paths.items()},
337
339
  children=dict(self.children),
338
340
  )
339
341
 
340
342
 
341
- @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True)
343
+ @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True, repr=False)
342
344
  class UserFileSystem(FileSystem):
343
345
  root: Node
344
346
  inbox: Node
345
347
  trash_bin: Node
346
348
 
347
- @classmethod
348
- def build_unsafe(cls, nodes: Iterable[Node]) -> Self:
349
- """Build a filesystem from a pre-ordered node stream without validation
350
-
351
- Nodes MUST be topologically sorted:
352
- - The first three nodes are ROOT, INBOX and TRASH_BIN.
353
- - Every parent appears before its descendants.
354
- """
355
- # Build fs using only 3 loops:
356
- # - 1. Create a map to all nodes and resolve their paths
357
- # - 2. Freeze children (list -> tuple)
358
- # - 3. Freeze inv paths (list -> tuple)
359
- root = inbox = trash_bin = None
360
- file_count = folder_count = 0
361
-
362
- paths: dict[NodeID, PurePosixPath] = {}
363
- inv_paths: dict[PurePosixPath, list[NodeID]] = {}
364
- deleted_ids: set[NodeID] = set()
365
- nodes_map: dict[NodeID, Node] = {}
366
- children: dict[NodeID, list[NodeID]] = {}
367
- trash_bin_id = None
368
-
369
- for node in nodes:
370
- nodes_map[node.id] = node
371
- if node.parent_id:
372
- children.setdefault(node.parent_id, []).append(node.id)
373
-
374
- path = None
375
- match node.type:
376
- case NodeType.FILE:
377
- file_count += 1
378
- case NodeType.FOLDER:
379
- folder_count += 1
380
- case NodeType.ROOT_FOLDER:
381
- path = POSIX_ROOT
382
- root = node
383
- case NodeType.INBOX:
384
- path = POSIX_ROOT / node.attributes.name
385
- inbox = node
386
- case NodeType.TRASH:
387
- path = POSIX_ROOT / node.attributes.name
388
- trash_bin_id = node.id
389
- trash_bin = node
390
- case _:
391
- raise RuntimeError # pyright: ignore[reportUnreachable]
392
-
393
- path = path or (paths[node.parent_id] / node.attributes.name)
394
- paths[node.id] = path
395
- inv_paths.setdefault(path, []).append(node.id)
396
- if node.parent_id == trash_bin_id or node.parent_id in deleted_ids:
397
- deleted_ids.add(node.id)
398
-
399
- assert root and inbox and trash_bin
400
- return cls(
401
- root=root,
402
- inbox=inbox,
403
- trash_bin=trash_bin,
404
- file_count=file_count,
405
- folder_count=folder_count,
406
- _nodes=MappingProxyType(nodes_map),
407
- _children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}),
408
- _paths=MappingProxyType(paths),
409
- _inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}),
410
- _deleted=frozenset(deleted_ids),
411
- )
412
-
413
349
 
414
350
  if __name__ == "__main__":
415
351
  import json
@@ -423,7 +359,5 @@ if __name__ == "__main__":
423
359
  print(f"Testing filesystem build of {len(nodes):_} nodes") # noqa: T201
424
360
 
425
361
  iterations = 1_000
426
- unsafe = timeit.timeit(lambda: UserFileSystem.build_unsafe(nodes), number=iterations)
427
- print(f"{iterations} [UNSAFE] calls took {unsafe:.4f}s") # noqa: T201
428
362
  safe = timeit.timeit(lambda: UserFileSystem.build(nodes), number=iterations)
429
363
  print(f"{iterations} [SAFE] calls took {safe:.4f}s") # noqa: T201
@@ -1,17 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
- from typing import TYPE_CHECKING, Final, cast
4
+ from typing import TYPE_CHECKING, Final, NamedTuple, cast
5
5
 
6
6
  from mega.crypto import b64_to_a32, decrypt_key
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from mega.data_structures import (
10
10
  ComposedFileKey,
11
- ComposedFolderKey,
12
11
  GetNodesResponse,
13
12
  Node,
14
13
  NodeID,
14
+ NodeKey,
15
15
  SharedKeys,
16
16
  UserID,
17
17
  )
@@ -21,6 +21,11 @@ _EXPORTED: Final = "EXP"
21
21
  # An special owner used for exported (AKA public) file/folders
22
22
 
23
23
 
24
+ class KeyLookup(NamedTuple):
25
+ full_key: ComposedFileKey | NodeKey
26
+ share_key: NodeKey | None
27
+
28
+
24
29
  @dataclasses.dataclass(slots=True, frozen=True)
25
30
  class MegaVault:
26
31
  master_key: tuple[int, ...] = ()
@@ -43,7 +48,7 @@ class MegaVault:
43
48
  if key := new_keys.get(node_id):
44
49
  self._shared_keys.setdefault(owner, {})[node_id] = key
45
50
 
46
- def __getitem__(self, node: Node) -> tuple[ComposedFileKey | ComposedFolderKey, tuple[int, ...] | None]:
51
+ def __getitem__(self, node: Node) -> KeyLookup:
47
52
  share_key: tuple[int, ...] | None = None
48
53
 
49
54
  # my files/folders
@@ -74,7 +79,7 @@ class MegaVault:
74
79
  if len(full_key) not in (4, 8):
75
80
  raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
76
81
 
77
- return cast("ComposedFileKey | ComposedFolderKey", full_key), share_key
82
+ return KeyLookup(cast("ComposedFileKey | NodeKey", full_key), share_key)
78
83
 
79
84
  def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
80
85
  self._shared_keys[_EXPORTED][node_id] = share_key
File without changes
File without changes