rclone-api 1.0.41__tar.gz → 1.0.43__tar.gz

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rclone-api might be problematic. Click here for more details.

Files changed (64) hide show
  1. {rclone_api-1.0.41 → rclone_api-1.0.43}/PKG-INFO +1 -1
  2. {rclone_api-1.0.41 → rclone_api-1.0.43}/pyproject.toml +1 -1
  3. rclone_api-1.0.43/src/rclone_api/group_files.py +147 -0
  4. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/rclone.py +3 -3
  5. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/util.py +0 -12
  6. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/PKG-INFO +1 -1
  7. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/SOURCES.txt +2 -0
  8. rclone_api-1.0.43/tests/test_group_files.py +101 -0
  9. {rclone_api-1.0.41 → rclone_api-1.0.43}/.aiderignore +0 -0
  10. {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/lint.yml +0 -0
  11. {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_macos.yml +0 -0
  12. {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_ubuntu.yml +0 -0
  13. {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_win.yml +0 -0
  14. {rclone_api-1.0.41 → rclone_api-1.0.43}/.gitignore +0 -0
  15. {rclone_api-1.0.41 → rclone_api-1.0.43}/.pylintrc +0 -0
  16. {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/launch.json +0 -0
  17. {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/settings.json +0 -0
  18. {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/tasks.json +0 -0
  19. {rclone_api-1.0.41 → rclone_api-1.0.43}/LICENSE +0 -0
  20. {rclone_api-1.0.41 → rclone_api-1.0.43}/MANIFEST.in +0 -0
  21. {rclone_api-1.0.41 → rclone_api-1.0.43}/README.md +0 -0
  22. {rclone_api-1.0.41 → rclone_api-1.0.43}/clean +0 -0
  23. {rclone_api-1.0.41 → rclone_api-1.0.43}/install +0 -0
  24. {rclone_api-1.0.41 → rclone_api-1.0.43}/lint +0 -0
  25. {rclone_api-1.0.41 → rclone_api-1.0.43}/requirements.testing.txt +0 -0
  26. {rclone_api-1.0.41 → rclone_api-1.0.43}/setup.cfg +0 -0
  27. {rclone_api-1.0.41 → rclone_api-1.0.43}/setup.py +0 -0
  28. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/__init__.py +0 -0
  29. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/assets/example.txt +0 -0
  30. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/cli.py +0 -0
  31. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/cmd/list_files.py +0 -0
  32. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/completed_process.py +0 -0
  33. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/config.py +0 -0
  34. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/convert.py +0 -0
  35. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/deprecated.py +0 -0
  36. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/diff.py +0 -0
  37. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/dir.py +0 -0
  38. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/dir_listing.py +0 -0
  39. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/exec.py +0 -0
  40. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/file.py +0 -0
  41. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/filelist.py +0 -0
  42. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/process.py +0 -0
  43. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/remote.py +0 -0
  44. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/rpath.py +0 -0
  45. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/walk.py +0 -0
  46. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  47. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/entry_points.txt +0 -0
  48. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/requires.txt +0 -0
  49. {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/top_level.txt +0 -0
  50. {rclone_api-1.0.41 → rclone_api-1.0.43}/test +0 -0
  51. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_cmd_list_files.py +0 -0
  52. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_copy.py +0 -0
  53. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_diff.py +0 -0
  54. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_is_synced.py +0 -0
  55. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_ls.py +0 -0
  56. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount.py +0 -0
  57. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount_s3.py +0 -0
  58. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount_webdav.py +0 -0
  59. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_obscure.py +0 -0
  60. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_remotes.py +0 -0
  61. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_serve_webdav.py +0 -0
  62. {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_walk.py +0 -0
  63. {rclone_api-1.0.41 → rclone_api-1.0.43}/tox.ini +0 -0
  64. {rclone_api-1.0.41 → rclone_api-1.0.43}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.41
3
+ Version: 1.0.43
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "python-dotenv>=1.0.0",
16
16
  ]
17
17
  # Change this with the version number bump.
18
- version = "1.0.41"
18
+ version = "1.0.43"
19
19
 
20
20
  [tool.setuptools]
21
21
  package-dir = {"" = "src"}
@@ -0,0 +1,147 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class FilePathParts:
6
+ """File path dataclass."""
7
+
8
+ remote: str
9
+ parents: list[str]
10
+ name: str
11
+
12
+
13
+ def parse_file(file_path: str) -> FilePathParts:
14
+ """Parse file path into parts."""
15
+ assert not file_path.endswith("/"), "This looks like a directory path"
16
+ parts = file_path.split(":")
17
+ remote = parts[0]
18
+ path = parts[1]
19
+ if path.startswith("/"):
20
+ path = path[1:]
21
+ parents = path.split("/")
22
+ if len(parents) == 1:
23
+ return FilePathParts(remote=remote, parents=[], name=parents[0])
24
+ name = parents.pop()
25
+ return FilePathParts(remote=remote, parents=parents, name=name)
26
+
27
+
28
+ class TreeNode:
29
+ def __init__(
30
+ self,
31
+ name: str,
32
+ child_nodes: dict[str, "TreeNode"] | None = None,
33
+ files: list[str] | None = None,
34
+ parent: "TreeNode | None" = None,
35
+ ):
36
+ self.name = name
37
+ self.child_nodes = child_nodes or {}
38
+ self.files = files or []
39
+ self.count = 0
40
+ self.parent = parent
41
+
42
+ def add_count(self):
43
+ self.count += 1
44
+ if self.parent:
45
+ self.parent.add_count()
46
+
47
+ def get_path(self) -> str:
48
+ paths_reversed: list[str] = [self.name]
49
+ node: TreeNode | None = self
50
+ assert node is not None
51
+ while node := node.parent:
52
+ paths_reversed.append(node.name)
53
+ return "/".join(reversed(paths_reversed))
54
+
55
+ def get_child_subpaths(self, parent_path: str | None = None) -> list[str]:
56
+ paths: list[str] = []
57
+ for child in self.child_nodes.values():
58
+ child_paths = child.get_child_subpaths(parent_path=child.name)
59
+ paths.extend(child_paths)
60
+ for file in self.files:
61
+ if parent_path:
62
+ file = f"{parent_path}/{file}"
63
+ paths.append(file)
64
+ return paths
65
+
66
+ def __repr__(self, indent: int = 0) -> str:
67
+ # return f"{self.name}: {self.count}, {len(self.children)}"
68
+ leftpad = " " * indent
69
+ msg = f"{leftpad}{self.name}: {self.count}"
70
+ if self.child_nodes:
71
+ # msg += f"\n {len(self.children)} children"
72
+ msg += "\n"
73
+ for child in self.child_nodes.values():
74
+ if isinstance(child, TreeNode):
75
+ msg += child.__repr__(indent + 2)
76
+ else:
77
+ msg += f"{leftpad} {child}\n"
78
+ return msg
79
+
80
+
81
+ def _merge(node: TreeNode, parent_path: str, out: dict[str, list[str]]) -> None:
82
+ parent_path = parent_path + "/" + node.name
83
+ if not node.child_nodes and not node.files:
84
+ return # done
85
+ if node.files:
86
+ # we saw files, to don't try to go any deeper.
87
+ filelist = out.setdefault(parent_path, [])
88
+ # for file in node.files:
89
+ # filelist.append(file)
90
+ # out[parent_path] = filelist
91
+ paths = node.get_child_subpaths()
92
+ for path in paths:
93
+ filelist.append(path)
94
+ out[parent_path] = filelist
95
+ return
96
+
97
+ n_child_nodes = len(node.child_nodes)
98
+
99
+ if n_child_nodes < 4:
100
+ # child = list(node.child_nodes.values())[0]
101
+ # _merge(child, parent_path, out)
102
+ # return
103
+ for child in node.child_nodes.values():
104
+ _merge(child, parent_path, out)
105
+ return
106
+
107
+ filelist = out.setdefault(parent_path, [])
108
+ # for file in node.files:
109
+ # filelist.append(file)
110
+ # out[parent_path] = filelist
111
+ paths = node.get_child_subpaths()
112
+ for path in paths:
113
+ filelist.append(path)
114
+ out[parent_path] = filelist
115
+ return
116
+
117
+
118
+ def _make_tree(files: list[str]) -> dict[str, TreeNode]:
119
+ tree: dict[str, TreeNode] = {}
120
+ for file in files:
121
+ parts = parse_file(file)
122
+ remote = parts.remote
123
+ node: TreeNode = tree.setdefault(remote, TreeNode(remote))
124
+ for parent in parts.parents:
125
+ is_last = parent == parts.parents[-1]
126
+ node = node.child_nodes.setdefault(parent, TreeNode(parent, parent=node))
127
+ if is_last:
128
+ node.files.append(parts.name)
129
+ node.add_count()
130
+ return tree
131
+
132
+
133
+ def group_files(files: list[str]) -> dict[str, list[str]]:
134
+ """split between filename and parent directory path"""
135
+ tree: dict[str, TreeNode] = _make_tree(files)
136
+ outpaths: dict[str, list[str]] = {}
137
+ for _, node in tree.items():
138
+ _merge(node, "", outpaths)
139
+ out: dict[str, list[str]] = {}
140
+ for path, files in outpaths.items():
141
+ # fixup path
142
+ assert path.startswith("/"), "Path should start with /"
143
+ path = path[1:]
144
+ # replace the first / with :
145
+ path = path.replace("/", ":", 1)
146
+ out[path] = files
147
+ return out
@@ -21,13 +21,13 @@ from rclone_api.diff import DiffItem, diff_stream_from_running_process
21
21
  from rclone_api.dir_listing import DirListing
22
22
  from rclone_api.exec import RcloneExec
23
23
  from rclone_api.file import File
24
+ from rclone_api.group_files import group_files
24
25
  from rclone_api.process import Process
25
26
  from rclone_api.remote import Remote
26
27
  from rclone_api.rpath import RPath
27
28
  from rclone_api.util import (
28
29
  get_rclone_exe,
29
30
  get_verbose,
30
- partition_files,
31
31
  to_path,
32
32
  wait_for_mount,
33
33
  )
@@ -218,7 +218,7 @@ class Rclone:
218
218
  if len(payload) == 0:
219
219
  return
220
220
 
221
- datalists: dict[str, list[str]] = partition_files(payload)
221
+ datalists: dict[str, list[str]] = group_files(payload)
222
222
  out: subprocess.CompletedProcess | None = None
223
223
 
224
224
  futures: list[Future] = []
@@ -297,7 +297,7 @@ class Rclone:
297
297
  )
298
298
  return CompletedProcess.from_subprocess(cp)
299
299
 
300
- datalists: dict[str, list[str]] = partition_files(payload)
300
+ datalists: dict[str, list[str]] = group_files(payload)
301
301
  completed_processes: list[subprocess.CompletedProcess] = []
302
302
  verbose = get_verbose(verbose)
303
303
 
@@ -129,15 +129,3 @@ def wait_for_mount(path: Path, mount_process: Any, timeout: int = 60) -> None:
129
129
  if path.exists():
130
130
  return
131
131
  raise TimeoutError(f"Path {path} did not exist after {timeout} seconds")
132
-
133
-
134
- def partition_files(files: list[str]) -> dict[str, list[str]]:
135
- """split between filename and parent directory path"""
136
- datalists: dict[str, list[str]] = {}
137
- for f in files:
138
- base = os.path.basename(f)
139
- parent_path = os.path.dirname(f)
140
- if parent_path not in datalists:
141
- datalists[parent_path] = []
142
- datalists[parent_path].append(base)
143
- return datalists
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.41
3
+ Version: 1.0.43
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -32,6 +32,7 @@ src/rclone_api/dir_listing.py
32
32
  src/rclone_api/exec.py
33
33
  src/rclone_api/file.py
34
34
  src/rclone_api/filelist.py
35
+ src/rclone_api/group_files.py
35
36
  src/rclone_api/process.py
36
37
  src/rclone_api/rclone.py
37
38
  src/rclone_api/remote.py
@@ -49,6 +50,7 @@ src/rclone_api/cmd/list_files.py
49
50
  tests/test_cmd_list_files.py
50
51
  tests/test_copy.py
51
52
  tests/test_diff.py
53
+ tests/test_group_files.py
52
54
  tests/test_is_synced.py
53
55
  tests/test_ls.py
54
56
  tests/test_mount.py
@@ -0,0 +1,101 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import unittest
6
+
7
+ from rclone_api.group_files import group_files
8
+
9
+
10
+ class GroupFilestest(unittest.TestCase):
11
+ """Test rclone functionality."""
12
+
13
+ def test_simple_group_files(self) -> None:
14
+ files = [
15
+ "dst:Bucket/subdir/file1.txt",
16
+ "dst:Bucket/subdir/file2.txt",
17
+ ]
18
+ groups: dict[str, list[str]] = group_files(files)
19
+ self.assertEqual(len(groups), 1)
20
+ # dst:/Bucket/subdir should be the key
21
+ self.assertIn("dst:Bucket/subdir", groups)
22
+ self.assertEqual(len(groups["dst:Bucket/subdir"]), 2)
23
+ expected_files = [
24
+ "file1.txt",
25
+ "file2.txt",
26
+ ]
27
+ self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
28
+ self.assertIn(expected_files[1], groups["dst:Bucket/subdir"])
29
+ print("done")
30
+
31
+ def test_different_paths(self) -> None:
32
+ files = [
33
+ "dst:Bucket/subdir/file1.txt",
34
+ "dst:Bucket/subdir2/file2.txt",
35
+ ]
36
+ groups: dict[str, list[str]] = group_files(files)
37
+ self.assertEqual(len(groups), 2)
38
+ # dst:/Bucket/subdir should be the key
39
+ self.assertIn("dst:Bucket/subdir", groups)
40
+ self.assertEqual(len(groups["dst:Bucket/subdir"]), 1)
41
+ expected_files = [
42
+ "file1.txt",
43
+ ]
44
+ self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
45
+ # dst:/Bucket/subdir2 should be the key
46
+ self.assertIn("dst:Bucket/subdir2", groups)
47
+ self.assertEqual(len(groups["dst:Bucket/subdir2"]), 1)
48
+
49
+ def test_two_big_directories(self) -> None:
50
+ files = [
51
+ "dst:Bucket/subdir/file1.txt",
52
+ "dst:Bucket/subdir/file2.txt",
53
+ "dst:Bucket/subdir2/file3.txt",
54
+ "dst:Bucket/subdir2/file4.txt",
55
+ ]
56
+
57
+ groups: dict[str, list[str]] = group_files(files)
58
+ self.assertEqual(len(groups), 2)
59
+ # dst:/Bucket/subdir should be the key
60
+ self.assertIn("dst:Bucket/subdir", groups)
61
+ self.assertEqual(len(groups["dst:Bucket/subdir"]), 2)
62
+ expected_files = [
63
+ "file1.txt",
64
+ "file2.txt",
65
+ ]
66
+ self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
67
+ self.assertIn(expected_files[1], groups["dst:Bucket/subdir"])
68
+ # dst:/Bucket/subdir2 should be the key
69
+ self.assertIn("dst:Bucket/subdir2", groups)
70
+ self.assertEqual(len(groups["dst:Bucket/subdir2"]), 2)
71
+ expected_files = [
72
+ "file3.txt",
73
+ "file4.txt",
74
+ ]
75
+ self.assertIn(expected_files[0], groups["dst:Bucket/subdir2"])
76
+ self.assertIn(expected_files[1], groups["dst:Bucket/subdir2"])
77
+ print("done")
78
+
79
+ def test_two_fine_grained(self) -> None:
80
+ files = [
81
+ "dst:TorrentBooks/libgenrs_nonfiction/204000/a2b20b2c89240ce81dec16091e18113e",
82
+ "dst:TorrentBooks/libgenrs_nonfiction/208000/155fe185bc03048b003a8e145ed097c8",
83
+ "dst:TorrentBooks/libgenrs_nonfiction/208001/155fe185bc03048b003a8e145ed097c8",
84
+ "dst:TorrentBooks/libgenrs_nonfiction/208002/155fe185bc03048b003a8e145ed097c8",
85
+ "dst:TorrentBooks/libgenrs_nonfiction/2080054/155fe185bc03048b003a8e145ed097c4",
86
+ ]
87
+ # expect that this all goes under the same parent
88
+ groups: dict[str, list[str]] = group_files(files)
89
+ self.assertEqual(len(groups), 1)
90
+ # dst:/Bucket/subdir should be the key
91
+ self.assertIn("dst:TorrentBooks/libgenrs_nonfiction", groups)
92
+ self.assertEqual(len(groups["dst:TorrentBooks/libgenrs_nonfiction"]), 5)
93
+ expected_files = [
94
+ "204000/a2b20b2c89240ce81dec16091e18113e",
95
+ "208000/155fe185bc03048b003a8e145ed097c8",
96
+ ]
97
+ self.assertIn(expected_files[0], groups["dst:TorrentBooks/libgenrs_nonfiction"])
98
+
99
+
100
+ if __name__ == "__main__":
101
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes