rclone-api 1.0.76__tar.gz → 1.0.77__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. {rclone_api-1.0.76 → rclone_api-1.0.77}/PKG-INFO +1 -1
  2. {rclone_api-1.0.76 → rclone_api-1.0.77}/pyproject.toml +1 -1
  3. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/__init__.py +3 -1
  4. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/completed_process.py +11 -0
  5. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/file.py +11 -4
  6. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/group_files.py +45 -1
  7. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/rclone.py +77 -2
  8. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/remote.py +3 -0
  9. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/types.py +14 -0
  10. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/util.py +1 -1
  11. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/PKG-INFO +1 -1
  12. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/SOURCES.txt +1 -0
  13. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_diff.py +23 -0
  14. rclone_api-1.0.77/tests/test_size_files.py +84 -0
  15. {rclone_api-1.0.76 → rclone_api-1.0.77}/.aiderignore +0 -0
  16. {rclone_api-1.0.76 → rclone_api-1.0.77}/.github/workflows/lint.yml +0 -0
  17. {rclone_api-1.0.76 → rclone_api-1.0.77}/.github/workflows/push_macos.yml +0 -0
  18. {rclone_api-1.0.76 → rclone_api-1.0.77}/.github/workflows/push_ubuntu.yml +0 -0
  19. {rclone_api-1.0.76 → rclone_api-1.0.77}/.github/workflows/push_win.yml +0 -0
  20. {rclone_api-1.0.76 → rclone_api-1.0.77}/.gitignore +0 -0
  21. {rclone_api-1.0.76 → rclone_api-1.0.77}/.pylintrc +0 -0
  22. {rclone_api-1.0.76 → rclone_api-1.0.77}/.vscode/launch.json +0 -0
  23. {rclone_api-1.0.76 → rclone_api-1.0.77}/.vscode/settings.json +0 -0
  24. {rclone_api-1.0.76 → rclone_api-1.0.77}/.vscode/tasks.json +0 -0
  25. {rclone_api-1.0.76 → rclone_api-1.0.77}/LICENSE +0 -0
  26. {rclone_api-1.0.76 → rclone_api-1.0.77}/MANIFEST.in +0 -0
  27. {rclone_api-1.0.76 → rclone_api-1.0.77}/README.md +0 -0
  28. {rclone_api-1.0.76 → rclone_api-1.0.77}/clean +0 -0
  29. {rclone_api-1.0.76 → rclone_api-1.0.77}/install +0 -0
  30. {rclone_api-1.0.76 → rclone_api-1.0.77}/lint +0 -0
  31. {rclone_api-1.0.76 → rclone_api-1.0.77}/requirements.testing.txt +0 -0
  32. {rclone_api-1.0.76 → rclone_api-1.0.77}/setup.cfg +0 -0
  33. {rclone_api-1.0.76 → rclone_api-1.0.77}/setup.py +0 -0
  34. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/assets/example.txt +0 -0
  35. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/cli.py +0 -0
  36. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/cmd/list_files.py +0 -0
  37. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/config.py +0 -0
  38. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/convert.py +0 -0
  39. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/deprecated.py +0 -0
  40. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/diff.py +0 -0
  41. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/dir.py +0 -0
  42. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/dir_listing.py +0 -0
  43. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/exec.py +0 -0
  44. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/filelist.py +0 -0
  45. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/process.py +0 -0
  46. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/rpath.py +0 -0
  47. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/scan_missing_folders.py +0 -0
  48. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api/walk.py +0 -0
  49. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  50. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/entry_points.txt +0 -0
  51. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/requires.txt +0 -0
  52. {rclone_api-1.0.76 → rclone_api-1.0.77}/src/rclone_api.egg-info/top_level.txt +0 -0
  53. {rclone_api-1.0.76 → rclone_api-1.0.77}/test +0 -0
  54. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_cmd_list_files.py +0 -0
  55. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_copy.py +0 -0
  56. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_copy_files.py +0 -0
  57. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_group_files.py +0 -0
  58. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_is_synced.py +0 -0
  59. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_ls.py +0 -0
  60. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_mount.py +0 -0
  61. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_mount_s3.py +0 -0
  62. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_mount_webdav.py +0 -0
  63. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_obscure.py +0 -0
  64. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_remote_control.py +0 -0
  65. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_remotes.py +0 -0
  66. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_scan_missing_folders.py +0 -0
  67. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_serve_webdav.py +0 -0
  68. {rclone_api-1.0.76 → rclone_api-1.0.77}/tests/test_walk.py +0 -0
  69. {rclone_api-1.0.76 → rclone_api-1.0.77}/tox.ini +0 -0
  70. {rclone_api-1.0.76 → rclone_api-1.0.77}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.76
3
+ Version: 1.0.77
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.76"
18
+ version = "1.0.77"
19
19
 
20
20
  [tool.setuptools]
21
21
  package-dir = {"" = "src"}
@@ -9,7 +9,7 @@ from .process import Process
9
9
  from .rclone import Rclone, rclone_verbose
10
10
  from .remote import Remote
11
11
  from .rpath import RPath
12
- from .types import ListingOption, Order
12
+ from .types import GroupingOption, ListingOption, Order, SizeResult
13
13
 
14
14
  __all__ = [
15
15
  "Rclone",
@@ -29,4 +29,6 @@ __all__ = [
29
29
  "ListingOption",
30
30
  "Order",
31
31
  "ListingOption",
32
+ "GroupingOption",
33
+ "SizeResult",
32
34
  ]
@@ -47,3 +47,14 @@ class CompletedProcess:
47
47
  if rtn != 0:
48
48
  return rtn
49
49
  return 0
50
+
51
+ def __str__(self) -> str:
52
+
53
+ cmd_strs: list[str] = []
54
+ rtn_cods: list[int] = []
55
+ for cp in self.completed:
56
+ cmd_strs.append(subprocess.list2cmdline(cp.args))
57
+ rtn_cods.append(cp.returncode)
58
+ msg = f"CompletedProcess: {len(cmd_strs)} commands\n"
59
+ msg += "\n".join([f"{cmd} -> {rtn}" for cmd, rtn in zip(cmd_strs, rtn_cods)])
60
+ return msg
@@ -40,10 +40,17 @@ class File:
40
40
 
41
41
  def to_string(self, include_remote: bool = True) -> str:
42
42
  """Convert the File to a string."""
43
- out = str(self.path)
44
- if not include_remote:
45
- _, out = out.split(":", 1)
46
- return out
43
+ # out = str(self.path)
44
+ remote = self.path.remote
45
+ rest = self.path.path
46
+ if include_remote:
47
+ return f"{remote.name}:{rest}"
48
+ return rest
49
+
50
+ @property
51
+ def size(self) -> int:
52
+ """Get the size of the file."""
53
+ return self.path.size
47
54
 
48
55
  def __str__(self) -> str:
49
56
  return str(self.path)
@@ -9,6 +9,19 @@ class FilePathParts:
9
9
  parents: list[str]
10
10
  name: str
11
11
 
12
+ def to_string(self, include_remote: bool, include_bucket: bool) -> str:
13
+ """Convert to string, may throw for not include_bucket=False."""
14
+ parents = list(self.parents)
15
+ if not include_bucket:
16
+ parents.pop(0)
17
+ path = "/".join(parents)
18
+ if path:
19
+ path += "/"
20
+ path += self.name
21
+ if include_remote:
22
+ return f"{self.remote}{path}"
23
+ return path
24
+
12
25
 
13
26
  def parse_file(file_path: str) -> FilePathParts:
14
27
  """Parse file path into parts."""
@@ -165,4 +178,35 @@ def group_files(files: list[str], fully_qualified: bool = True) -> dict[str, lis
165
178
  return out
166
179
 
167
180
 
168
- __all__ = ["group_files"]
181
+ def group_under_remote(
182
+ files: list[str], fully_qualified: bool = True
183
+ ) -> dict[str, list[str]]:
184
+ """split between filename and remote"""
185
+ assert fully_qualified is True, "Not implemented for fully_qualified=False"
186
+ out: dict[str, list[str]] = {}
187
+ for file in files:
188
+ parsed = parse_file(file)
189
+ remote = f"{parsed.remote}:"
190
+ file_list = out.setdefault(remote, [])
191
+ file_list.append(parsed.to_string(include_remote=False, include_bucket=True))
192
+ return out
193
+
194
+
195
+ def group_under_remote_bucket(
196
+ files: list[str], fully_qualified: bool = True
197
+ ) -> dict[str, list[str]]:
198
+ """split between filename and bucket"""
199
+ assert fully_qualified is True, "Not implemented for fully_qualified=False"
200
+ out: dict[str, list[str]] = {}
201
+ for file in files:
202
+ parsed = parse_file(file)
203
+ remote = f"{parsed.remote}:"
204
+ parts = parsed.parents
205
+ bucket = parts[0]
206
+ remote_bucket = f"{remote}{bucket}"
207
+ file_list = out.setdefault(remote_bucket, [])
208
+ file_list.append(parsed.to_string(include_remote=False, include_bucket=False))
209
+ return out
210
+
211
+
212
+ __all__ = ["group_files", "group_under_remote", "group_under_remote_bucket"]
@@ -22,11 +22,21 @@ from rclone_api.diff import DiffItem, DiffOption, diff_stream_from_running_proce
22
22
  from rclone_api.dir_listing import DirListing
23
23
  from rclone_api.exec import RcloneExec
24
24
  from rclone_api.file import File
25
- from rclone_api.group_files import group_files
25
+ from rclone_api.group_files import (
26
+ group_files,
27
+ group_under_remote,
28
+ group_under_remote_bucket,
29
+ )
26
30
  from rclone_api.process import Process
27
31
  from rclone_api.remote import Remote
28
32
  from rclone_api.rpath import RPath
29
- from rclone_api.types import ListingOption, ModTimeStrategy, Order
33
+ from rclone_api.types import (
34
+ GroupingOption,
35
+ ListingOption,
36
+ ModTimeStrategy,
37
+ Order,
38
+ SizeResult,
39
+ )
30
40
  from rclone_api.util import (
31
41
  get_check,
32
42
  get_rclone_exe,
@@ -827,3 +837,68 @@ class Rclone:
827
837
  if proc.poll() is not None:
828
838
  raise ValueError("NFS serve process failed to start")
829
839
  return proc
840
+
841
+ def size_files(
842
+ self,
843
+ files: list[str],
844
+ fast_list: bool = True,
845
+ other_args: list[str] | None = None,
846
+ grouping: GroupingOption = GroupingOption.BUCKET,
847
+ check: bool | None = False,
848
+ verbose: bool | None = None,
849
+ ) -> SizeResult:
850
+ """Get the size of a list of files. Example of files items: "remote:bucket/to/file"."""
851
+ verbose = get_verbose(verbose)
852
+ check = get_check(check)
853
+ file_list: dict[str, list[str]]
854
+ if grouping == GroupingOption.BUCKET:
855
+ file_list = group_under_remote_bucket(files)
856
+ elif grouping == GroupingOption.REMOTE:
857
+ file_list = group_under_remote(files)
858
+ all_files: list[File] = []
859
+ for src_path, files in file_list.items():
860
+ cmd = ["lsjson", src_path, "--files-only", "-R"]
861
+ with TemporaryDirectory() as tmpdir:
862
+ # print("files: " + ",".join(files))
863
+ include_files_txt = Path(tmpdir) / "include_files.txt"
864
+ include_files_txt.write_text("\n".join(files), encoding="utf-8")
865
+ cmd += ["--files-from", str(include_files_txt)]
866
+ if fast_list:
867
+ cmd.append("--fast-list")
868
+ if other_args:
869
+ cmd += other_args
870
+ cp = self._run(cmd, check=check)
871
+
872
+ if cp.returncode != 0:
873
+ if check:
874
+ raise ValueError(f"Error getting file sizes: {cp.stderr}")
875
+ else:
876
+ warnings.warn(f"Error getting file sizes: {cp.stderr}")
877
+ stdout = cp.stdout
878
+ pieces = src_path.split(":", 1)
879
+ remote_name = pieces[0]
880
+ parent_path: str | None
881
+ if len(pieces) > 1:
882
+ parent_path = pieces[1]
883
+ else:
884
+ parent_path = None
885
+ remote = Remote(name=remote_name, rclone=self)
886
+ paths: list[RPath] = RPath.from_json_str(
887
+ stdout, remote, parent_path=parent_path
888
+ )
889
+ # print(paths)
890
+ all_files += [File(p) for p in paths]
891
+ file_sizes: dict[str, int] = {}
892
+ f: File
893
+ for f in all_files:
894
+ p = f.to_string(include_remote=True)
895
+ if p in file_sizes:
896
+ warnings.warn(f"Duplicate file found: {p}")
897
+ continue
898
+ size = f.size
899
+ if size == 0:
900
+ warnings.warn(f"File size is 0: {p}")
901
+ file_sizes[p] = f.size
902
+ total_size = sum(file_sizes.values())
903
+ out: SizeResult = SizeResult(total_size=total_size, file_sizes=file_sizes)
904
+ return out
@@ -7,6 +7,9 @@ class Remote:
7
7
  def __init__(self, name: str, rclone: Any) -> None:
8
8
  from rclone_api.rclone import Rclone
9
9
 
10
+ if ":" in name:
11
+ raise ValueError("Remote name cannot contain ':'")
12
+
10
13
  assert isinstance(rclone, Rclone)
11
14
  self.name = name
12
15
  self.rclone: Rclone = rclone
@@ -1,3 +1,4 @@
1
+ from dataclasses import dataclass
1
2
  from enum import Enum
2
3
 
3
4
 
@@ -16,3 +17,16 @@ class Order(Enum):
16
17
  NORMAL = "normal"
17
18
  REVERSE = "reverse"
18
19
  RANDOM = "random"
20
+
21
+
22
+ class GroupingOption(Enum):
23
+ BUCKET = "bucket"
24
+ REMOTE = "remote"
25
+
26
+
27
+ @dataclass
28
+ class SizeResult:
29
+ """Size result dataclass."""
30
+
31
+ total_size: int
32
+ file_sizes: dict[str, int]
@@ -104,7 +104,7 @@ def rclone_execute(
104
104
  )
105
105
  if verbose:
106
106
  cmd_str = subprocess.list2cmdline(cmd)
107
- print(f"Running: {cmd_str}")
107
+ print(f"\nRunning: {cmd_str}")
108
108
  cp = subprocess.run(
109
109
  cmd, capture_output=capture, encoding="utf-8", check=False, shell=False
110
110
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.76
3
+ Version: 1.0.77
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -64,4 +64,5 @@ tests/test_remote_control.py
64
64
  tests/test_remotes.py
65
65
  tests/test_scan_missing_folders.py
66
66
  tests/test_serve_webdav.py
67
+ tests/test_size_files.py
67
68
  tests/test_walk.py
@@ -70,6 +70,29 @@ class RcloneDiffTests(unittest.TestCase):
70
70
  msg = "\n".join([str(item) for item in all])
71
71
  print(msg)
72
72
 
73
+ def test_min_max_size(self) -> None:
74
+ rclone = Rclone(_generate_rclone_config())
75
+ item: DiffItem
76
+ all: list[DiffItem] = list(
77
+ rclone.diff(
78
+ "dst:rclone-api-unit-test", "dst:rclone-api-unit-test", min_size="70M"
79
+ )
80
+ )
81
+ for item in all:
82
+ if "internaly_ai_alignment.mp4" in item.path:
83
+ break
84
+ else:
85
+ self.fail("internaly_ai_alignment.mp4 not found")
86
+ all.clear()
87
+ all = list(
88
+ rclone.diff(
89
+ "dst:rclone-api-unit-test", "dst:rclone-api-unit-test", max_size="70M"
90
+ )
91
+ )
92
+ for item in all:
93
+ if "internaly_ai_alignment.mp4" in item.path:
94
+ self.fail("internaly_ai_alignment.mp4 not filtered")
95
+
73
96
  def test_diff_missing_on_dst(self) -> None:
74
97
  rclone = Rclone(_generate_rclone_config())
75
98
  item: DiffItem
@@ -0,0 +1,84 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import unittest
7
+
8
+ from dotenv import load_dotenv
9
+
10
+ from rclone_api import Config, DirListing, Rclone, SizeResult
11
+
12
+ load_dotenv()
13
+
14
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
15
+
16
+
17
+ def _generate_rclone_config() -> Config:
18
+
19
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
20
+
21
+ # Load additional environment variables
22
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
23
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
24
+ # BUCKET_URL = os.getenv("BUCKET_URL")
25
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
26
+
27
+ config_text = f"""
28
+ [dst]
29
+ type = s3
30
+ provider = DigitalOcean
31
+ access_key_id = {BUCKET_KEY_PUBLIC}
32
+ secret_access_key = {BUCKET_KEY_SECRET}
33
+ endpoint = {BUCKET_URL}
34
+ """
35
+
36
+ out = Config(config_text)
37
+ return out
38
+
39
+
40
+ class RcloneSizeFilesTester(unittest.TestCase):
41
+ """Test rclone functionality."""
42
+
43
+ def setUp(self) -> None:
44
+ """Check if all required environment variables are set before running tests."""
45
+ required_vars = [
46
+ "BUCKET_NAME",
47
+ "BUCKET_KEY_SECRET",
48
+ "BUCKET_KEY_PUBLIC",
49
+ "BUCKET_URL",
50
+ ]
51
+ missing = [var for var in required_vars if not os.getenv(var)]
52
+ if missing:
53
+ self.skipTest(
54
+ f"Missing required environment variables: {', '.join(missing)}"
55
+ )
56
+ os.environ["RCLONE_API_VERBOSE"] = "1"
57
+
58
+ def test_size(self) -> None:
59
+ rclone = Rclone(_generate_rclone_config())
60
+ # rclone.walk
61
+ dirlisting: DirListing
62
+ is_first = True
63
+ files: list[str] = []
64
+ for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
65
+ if is_first:
66
+ # assert just one file
67
+ # assert len(dirlisting.files) == 1
68
+ self.assertEqual(len(dirlisting.files), 1)
69
+ # assert it's first.txt
70
+ self.assertEqual(dirlisting.files[0].name, "first.txt")
71
+ is_first = False
72
+ # print(dirlisting)
73
+ for file in dirlisting.files:
74
+ files.append(str(file))
75
+
76
+ # print(files)
77
+
78
+ size_map: SizeResult = rclone.size_files(files, check=True)
79
+ print(size_map)
80
+ print("done")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ 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