rclone-api 1.5.8__py3-none-any.whl

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 (61) hide show
  1. rclone_api/__init__.py +951 -0
  2. rclone_api/assets/example.txt +1 -0
  3. rclone_api/cli.py +15 -0
  4. rclone_api/cmd/analyze.py +51 -0
  5. rclone_api/cmd/copy_large_s3.py +111 -0
  6. rclone_api/cmd/copy_large_s3_finish.py +81 -0
  7. rclone_api/cmd/list_files.py +27 -0
  8. rclone_api/cmd/save_to_db.py +77 -0
  9. rclone_api/completed_process.py +60 -0
  10. rclone_api/config.py +87 -0
  11. rclone_api/convert.py +31 -0
  12. rclone_api/db/__init__.py +3 -0
  13. rclone_api/db/db.py +277 -0
  14. rclone_api/db/models.py +57 -0
  15. rclone_api/deprecated.py +24 -0
  16. rclone_api/detail/copy_file_parts_resumable.py +42 -0
  17. rclone_api/detail/walk.py +116 -0
  18. rclone_api/diff.py +164 -0
  19. rclone_api/dir.py +113 -0
  20. rclone_api/dir_listing.py +66 -0
  21. rclone_api/exec.py +40 -0
  22. rclone_api/experimental/flags.py +89 -0
  23. rclone_api/experimental/flags_base.py +58 -0
  24. rclone_api/file.py +205 -0
  25. rclone_api/file_item.py +68 -0
  26. rclone_api/file_part.py +198 -0
  27. rclone_api/file_stream.py +52 -0
  28. rclone_api/filelist.py +30 -0
  29. rclone_api/group_files.py +256 -0
  30. rclone_api/http_server.py +244 -0
  31. rclone_api/install.py +95 -0
  32. rclone_api/log.py +44 -0
  33. rclone_api/mount.py +55 -0
  34. rclone_api/mount_util.py +247 -0
  35. rclone_api/process.py +187 -0
  36. rclone_api/rclone_impl.py +1285 -0
  37. rclone_api/remote.py +21 -0
  38. rclone_api/rpath.py +102 -0
  39. rclone_api/s3/api.py +109 -0
  40. rclone_api/s3/basic_ops.py +61 -0
  41. rclone_api/s3/chunk_task.py +187 -0
  42. rclone_api/s3/create.py +107 -0
  43. rclone_api/s3/multipart/file_info.py +7 -0
  44. rclone_api/s3/multipart/finished_piece.py +69 -0
  45. rclone_api/s3/multipart/info_json.py +239 -0
  46. rclone_api/s3/multipart/merge_state.py +147 -0
  47. rclone_api/s3/multipart/upload_info.py +62 -0
  48. rclone_api/s3/multipart/upload_parts_inline.py +356 -0
  49. rclone_api/s3/multipart/upload_parts_resumable.py +304 -0
  50. rclone_api/s3/multipart/upload_parts_server_side_merge.py +546 -0
  51. rclone_api/s3/multipart/upload_state.py +165 -0
  52. rclone_api/s3/types.py +67 -0
  53. rclone_api/scan_missing_folders.py +153 -0
  54. rclone_api/types.py +402 -0
  55. rclone_api/util.py +324 -0
  56. rclone_api-1.5.8.dist-info/LICENSE +21 -0
  57. rclone_api-1.5.8.dist-info/METADATA +969 -0
  58. rclone_api-1.5.8.dist-info/RECORD +61 -0
  59. rclone_api-1.5.8.dist-info/WHEEL +5 -0
  60. rclone_api-1.5.8.dist-info/entry_points.txt +5 -0
  61. rclone_api-1.5.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ Example assets that will be deployed with python code.
rclone_api/cli.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ Main entry point.
3
+ """
4
+
5
+ import sys
6
+
7
+
8
+ def main() -> int:
9
+ """Main entry point for the template_python_cmd package."""
10
+ print("Replace with a CLI entry point.")
11
+ return 0
12
+
13
+
14
+ if __name__ == "__main__":
15
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ import argparse
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from rclone_api import Rclone
6
+
7
+
8
+ @dataclass
9
+ class Args:
10
+ config: Path
11
+ path: str
12
+
13
+ def __post_init__(self):
14
+ if not self.config.exists():
15
+ raise FileNotFoundError(f"Config file not found: {self.config}")
16
+
17
+
18
+ def list_files(rclone: Rclone, path: str):
19
+ """List files in a remote path."""
20
+
21
+ with rclone.ls_stream(path, fast_list=True) as files:
22
+ for file_item in files:
23
+ print(file_item.path, "", file_item.size, file_item.mod_time)
24
+
25
+
26
+ def _parse_args() -> Args:
27
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
28
+ parser.add_argument(
29
+ "--config", help="Path to rclone config file", type=Path, default="rclone.conf"
30
+ )
31
+ parser.add_argument("path", help="Remote path to list")
32
+ tmp = parser.parse_args()
33
+ return Args(config=tmp.config, path=tmp.path)
34
+
35
+
36
+ def main() -> int:
37
+ """Main entry point."""
38
+ args = _parse_args()
39
+ path = args.path
40
+ rclone = Rclone(Path(args.config))
41
+ list_files(rclone, path)
42
+ return 0
43
+
44
+
45
+ if __name__ == "__main__":
46
+ import sys
47
+
48
+ cwd = Path(".").absolute()
49
+ print(f"cwd: {cwd}")
50
+ sys.argv.append("dst:TorrentBooks")
51
+ main()
@@ -0,0 +1,111 @@
1
+ import argparse
2
+ import sys
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from rclone_api import Rclone, SizeSuffix
7
+
8
+
9
+ @dataclass
10
+ class Args:
11
+ config_path: Path
12
+ src: str
13
+ dst: str
14
+ chunk_size: SizeSuffix
15
+ threads: int
16
+ retries: int
17
+ save_state_json: Path
18
+ verbose: bool
19
+
20
+
21
+ def list_files(rclone: Rclone, path: str):
22
+ """List files in a remote path."""
23
+ for dirlisting in rclone.walk(path):
24
+ for file in dirlisting.files:
25
+ print(file.path)
26
+
27
+
28
+ def _parse_args() -> Args:
29
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
30
+ parser.add_argument("src", help="File to copy")
31
+ parser.add_argument("dst", help="Destination file")
32
+ parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
33
+ parser.add_argument(
34
+ "--config", help="Path to rclone config file", type=Path, required=False
35
+ )
36
+ parser.add_argument(
37
+ "--chunk-size",
38
+ help="Chunk size that will be read and uploaded in SizeSuffix form, too low or too high will cause issues",
39
+ type=str,
40
+ default="128MB", # if this is too low or too high an s3 service
41
+ )
42
+ parser.add_argument(
43
+ "--threads",
44
+ help="Max number of chunks to upload in parallel to the destination, each chunk is uploaded in a separate thread",
45
+ type=int,
46
+ default=8,
47
+ )
48
+ parser.add_argument("--retries", help="Number of retries", type=int, default=3)
49
+ parser.add_argument(
50
+ "--resume-json",
51
+ help="Path to resumable JSON file",
52
+ type=Path,
53
+ default="resume.json",
54
+ )
55
+
56
+ args = parser.parse_args()
57
+ config: Path | None = args.config
58
+ if config is None:
59
+ config = Path("rclone.conf")
60
+ if not config.exists():
61
+ raise FileNotFoundError(f"Config file not found: {config}")
62
+ assert config is not None
63
+ out = Args(
64
+ config_path=config,
65
+ src=args.src,
66
+ dst=args.dst,
67
+ threads=args.threads,
68
+ chunk_size=SizeSuffix(args.chunk_size),
69
+ retries=args.retries,
70
+ save_state_json=args.resume_json,
71
+ verbose=args.verbose,
72
+ )
73
+ return out
74
+
75
+
76
+ def main() -> int:
77
+ """Main entry point."""
78
+ args = _parse_args()
79
+ rclone = Rclone(rclone_conf=args.config_path)
80
+ # unit_chunk = args.chunk_size / args.threads
81
+ # rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
82
+ # src=args.src,
83
+ # dst=args.dst,
84
+ # chunk_size=args.chunk_size,
85
+ # read_threads=args.read_threads,
86
+ # write_threads=args.write_threads,
87
+ # retries=args.retries,
88
+ # save_state_json=args.save_state_json,
89
+ # verbose=args.verbose,
90
+ # )
91
+ err: Exception | None = rclone.copy_file_s3_resumable(
92
+ src=args.src,
93
+ dst=args.dst,
94
+ )
95
+ if err is not None:
96
+ print(f"Error: {err}")
97
+ raise err
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+
103
+ sys.argv.append("--config")
104
+ sys.argv.append("rclone.conf")
105
+ sys.argv.append(
106
+ "45061:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
107
+ )
108
+ sys.argv.append(
109
+ "dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
110
+ )
111
+ main()
@@ -0,0 +1,81 @@
1
+ import argparse
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from rclone_api import Rclone
6
+ from rclone_api.s3.multipart.upload_parts_server_side_merge import (
7
+ s3_server_side_multi_part_merge,
8
+ )
9
+
10
+
11
+ @dataclass
12
+ class Args:
13
+ config_path: Path
14
+ src: str # like dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/ (info.json will be located here)
15
+ verbose: bool
16
+
17
+ def __repr__(self):
18
+ return f"Args(config_path={self.config_path}, src={self.src}, verbose={self.verbose})"
19
+
20
+ def __str__(self):
21
+ return repr(self)
22
+
23
+
24
+ def list_files(rclone: Rclone, path: str):
25
+ """List files in a remote path."""
26
+ for dirlisting in rclone.walk(path):
27
+ for file in dirlisting.files:
28
+ print(file.path)
29
+
30
+
31
+ def _parse_args() -> Args:
32
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
33
+ parser.add_argument("src", help="Directory that holds the info.json file")
34
+ parser.add_argument("--no-verbose", help="Verbose output", action="store_true")
35
+ parser.add_argument(
36
+ "--config", help="Path to rclone config file", type=Path, required=False
37
+ )
38
+ args = parser.parse_args()
39
+ config: Path | None = args.config
40
+ if config is None:
41
+ config = Path("rclone.conf")
42
+ if not config.exists():
43
+ raise FileNotFoundError(f"Config file not found: {config}")
44
+ assert config is not None
45
+ out = Args(
46
+ config_path=config,
47
+ src=args.src,
48
+ verbose=not args.no_verbose,
49
+ )
50
+ return out
51
+
52
+
53
+ def _get_info_path(src: str) -> str:
54
+ if src.endswith("/"):
55
+ src = src[:-1]
56
+ info_path = f"{src}/info.json"
57
+ return info_path
58
+
59
+
60
+ def main() -> int:
61
+ """Main entry point."""
62
+ print("Starting...")
63
+ args = _parse_args()
64
+ print(f"args: {args}")
65
+ rclone = Rclone(rclone_conf=args.config_path)
66
+ info_path = _get_info_path(src=args.src)
67
+ s3_server_side_multi_part_merge(
68
+ rclone=rclone.impl, info_path=info_path, max_workers=5, verbose=args.verbose
69
+ )
70
+ return 0
71
+
72
+
73
+ if __name__ == "__main__":
74
+ import sys
75
+
76
+ sys.argv.append("--config")
77
+ sys.argv.append("rclone.conf")
78
+ sys.argv.append(
79
+ "dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/"
80
+ )
81
+ main()
@@ -0,0 +1,27 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from rclone_api import Rclone
5
+
6
+
7
+ def list_files(rclone: Rclone, path: str):
8
+ """List files in a remote path."""
9
+ for dirlisting in rclone.walk(path):
10
+ for file in dirlisting.files:
11
+ print(file.path)
12
+
13
+
14
+ def _parse_args() -> argparse.Namespace:
15
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
16
+ parser.add_argument("--config", help="Path to rclone config file", required=True)
17
+ parser.add_argument("path", help="Remote path to list")
18
+ return parser.parse_args()
19
+
20
+
21
+ def main() -> int:
22
+ """Main entry point."""
23
+ args = _parse_args()
24
+ path = args.path
25
+ rclone = Rclone(Path(args.config))
26
+ list_files(rclone, path)
27
+ return 0
@@ -0,0 +1,77 @@
1
+ import argparse
2
+ import os
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ from rclone_api import Rclone
9
+
10
+ # load_dotenv()
11
+
12
+
13
+ # DB_URL = "sqlite:///data.db"
14
+
15
+ # os.environ["DB_URL"] = "sqlite:///data.db"
16
+
17
+
18
+ def _db_url_from_env_or_raise() -> str:
19
+ load_dotenv(Path(".env"))
20
+ db_url = os.getenv("DB_URL")
21
+ if db_url is None:
22
+ raise ValueError("DB_URL not set")
23
+ return db_url
24
+
25
+
26
+ @dataclass
27
+ class Args:
28
+ config: Path
29
+ path: str
30
+ db_url: str
31
+ fast_list: bool
32
+
33
+ def __post_init__(self):
34
+ if not self.config.exists():
35
+ raise FileNotFoundError(f"Config file not found: {self.config}")
36
+
37
+
38
+ def fill_db(rclone: Rclone, path: str, fast_list: bool) -> None:
39
+ """List files in a remote path."""
40
+ # db = DB(_db_url_from_env_or_raise())
41
+ db_url = _db_url_from_env_or_raise()
42
+ rclone.save_to_db(src=path, db_url=db_url, fast_list=fast_list)
43
+
44
+
45
+ def _parse_args() -> Args:
46
+ parser = argparse.ArgumentParser(description="List files in a remote path.")
47
+ parser.add_argument(
48
+ "--config", help="Path to rclone config file", type=Path, default="rclone.conf"
49
+ )
50
+ parser.add_argument("--db", help="Database URL", type=str, default=None)
51
+ parser.add_argument("--fast-list", help="Use fast list", action="store_true")
52
+ parser.add_argument("path", help="Remote path to list")
53
+ tmp = parser.parse_args()
54
+ return Args(
55
+ config=tmp.config,
56
+ path=tmp.path,
57
+ db_url=tmp.db if tmp.db is not None else _db_url_from_env_or_raise(),
58
+ fast_list=tmp.fast_list,
59
+ )
60
+
61
+
62
+ def main() -> int:
63
+ """Main entry point."""
64
+ args = _parse_args()
65
+ path = args.path
66
+ rclone = Rclone(Path(args.config))
67
+ fill_db(rclone=rclone, path=path, fast_list=args.fast_list)
68
+ return 0
69
+
70
+
71
+ if __name__ == "__main__":
72
+ import sys
73
+
74
+ cwd = Path(".").absolute()
75
+ print(f"cwd: {cwd}")
76
+ sys.argv.append("dst:TorrentBooks/meta")
77
+ main()
@@ -0,0 +1,60 @@
1
+ import subprocess
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class CompletedProcess:
7
+ completed: list[subprocess.CompletedProcess]
8
+
9
+ @property
10
+ def ok(self) -> bool:
11
+ return all([p.returncode == 0 for p in self.completed])
12
+
13
+ @staticmethod
14
+ def from_subprocess(process: subprocess.CompletedProcess) -> "CompletedProcess":
15
+ return CompletedProcess(completed=[process])
16
+
17
+ def failed(self) -> list[subprocess.CompletedProcess]:
18
+ return [p for p in self.completed if p.returncode != 0]
19
+
20
+ def successes(self) -> list[subprocess.CompletedProcess]:
21
+ return [p for p in self.completed if p.returncode == 0]
22
+
23
+ @property
24
+ def stdout(self) -> str:
25
+ tmp: list[str] = []
26
+ for cp in self.completed:
27
+ stdout = cp.stdout
28
+ if stdout is not None:
29
+ tmp.append(stdout)
30
+ return "\n".join(tmp)
31
+
32
+ @property
33
+ def stderr(self) -> str:
34
+ tmp: list[str] = []
35
+ for cp in self.completed:
36
+ stderr = cp.stderr
37
+ if stderr is not None:
38
+ tmp.append(stderr)
39
+ return "\n".join(tmp)
40
+
41
+ @property
42
+ def returncode(self) -> int | None:
43
+ for cp in self.completed:
44
+ rtn = cp.returncode
45
+ if rtn is None:
46
+ return None
47
+ if rtn != 0:
48
+ return rtn
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
rclone_api/config.py ADDED
@@ -0,0 +1,87 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List
3
+
4
+
5
+ @dataclass
6
+ class Section:
7
+ name: str
8
+ data: Dict[str, str] = field(default_factory=dict)
9
+
10
+ def add(self, key: str, value: str) -> None:
11
+ self.data[key] = value
12
+
13
+ def type(self) -> str:
14
+ return self.data["type"]
15
+
16
+ def provider(self) -> str | None:
17
+ return self.data.get("provider")
18
+
19
+ def access_key_id(self) -> str:
20
+ if "access_key_id" in self.data:
21
+ return self.data["access_key_id"]
22
+ elif "account" in self.data:
23
+ return self.data["account"]
24
+ raise KeyError("No access key found")
25
+
26
+ def secret_access_key(self) -> str:
27
+ # return self.data["secret_access_key"]
28
+ if "secret_access_key" in self.data:
29
+ return self.data["secret_access_key"]
30
+ elif "key" in self.data:
31
+ return self.data["key"]
32
+ raise KeyError("No secret access key found")
33
+
34
+ def endpoint(self) -> str | None:
35
+ return self.data.get("endpoint")
36
+
37
+
38
+ @dataclass
39
+ class Parsed:
40
+ # sections: List[ParsedSection]
41
+ sections: dict[str, Section]
42
+
43
+ @staticmethod
44
+ def parse(content: str) -> "Parsed":
45
+ return parse_rclone_config(content)
46
+
47
+
48
+ @dataclass
49
+ class Config:
50
+ """Rclone configuration dataclass."""
51
+
52
+ text: str
53
+
54
+ def parse(self) -> Parsed:
55
+ return Parsed.parse(self.text)
56
+
57
+
58
+ def parse_rclone_config(content: str) -> Parsed:
59
+ """
60
+ Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
61
+
62
+ Each section in the file starts with a line like [section_name]
63
+ followed by key=value pairs.
64
+ """
65
+ sections: List[Section] = []
66
+ current_section: Section | None = None
67
+
68
+ lines = content.splitlines()
69
+ for line in lines:
70
+ line = line.strip()
71
+ # Skip empty lines and comments (assumed to start with '#' or ';')
72
+ if not line or line.startswith(("#", ";")):
73
+ continue
74
+ # New section header detected
75
+ if line.startswith("[") and line.endswith("]"):
76
+ section_name = line[1:-1].strip()
77
+ current_section = Section(name=section_name)
78
+ sections.append(current_section)
79
+ elif "=" in line and current_section is not None:
80
+ # Parse key and value, splitting only on the first '=' found
81
+ key, value = line.split("=", 1)
82
+ current_section.add(key.strip(), value.strip())
83
+
84
+ data: dict[str, Section] = {}
85
+ for section in sections:
86
+ data[section.name] = section
87
+ return Parsed(sections=data)
rclone_api/convert.py ADDED
@@ -0,0 +1,31 @@
1
+ from rclone_api.dir import Dir
2
+ from rclone_api.file import File
3
+ from rclone_api.remote import Remote
4
+
5
+
6
+ def convert_to_filestr_list(files: str | File | list[str] | list[File]) -> list[str]:
7
+ out: list[str] = []
8
+ if isinstance(files, str):
9
+ out.append(files)
10
+ elif isinstance(files, File):
11
+ out.append(str(files.path))
12
+ elif isinstance(files, list):
13
+ for f in files:
14
+ if isinstance(f, File):
15
+ f = str(f.path)
16
+ out.append(f)
17
+ else:
18
+ raise ValueError(f"Invalid type for file: {type(files)}")
19
+ return out
20
+
21
+
22
+ def convert_to_str(file_or_dir: str | File | Dir | Remote) -> str:
23
+ if isinstance(file_or_dir, str):
24
+ return file_or_dir
25
+ if isinstance(file_or_dir, File):
26
+ return str(file_or_dir.path)
27
+ if isinstance(file_or_dir, Dir):
28
+ return str(file_or_dir.path)
29
+ if isinstance(file_or_dir, Remote):
30
+ return str(file_or_dir)
31
+ raise ValueError(f"Invalid type for file_or_dir: {type(file_or_dir)}")
@@ -0,0 +1,3 @@
1
+ from .db import DB
2
+
3
+ __all__ = ["DB"]