rclone-api 1.0.88__tar.gz → 1.0.90__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.
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.gitignore +5 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/PKG-INFO +2 -3
- {rclone_api-1.0.88 → rclone_api-1.0.90}/pyproject.toml +9 -2
- {rclone_api-1.0.88 → rclone_api-1.0.90}/setup.py +0 -1
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/__init__.py +5 -1
- rclone_api-1.0.90/src/rclone_api/cmd/copy_large_s3.py +99 -0
- rclone_api-1.0.90/src/rclone_api/config.py +87 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/group_files.py +4 -1
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/rclone.py +238 -49
- rclone_api-1.0.90/src/rclone_api/s3/api.py +73 -0
- rclone_api-1.0.90/src/rclone_api/s3/basic_ops.py +61 -0
- rclone_api-1.0.90/src/rclone_api/s3/chunk_uploader.py +538 -0
- rclone_api-1.0.90/src/rclone_api/s3/create.py +69 -0
- rclone_api-1.0.90/src/rclone_api/s3/types.py +58 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/types.py +5 -3
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/util.py +32 -4
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/PKG-INFO +2 -3
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/SOURCES.txt +11 -2
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/entry_points.txt +1 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/requires.txt +1 -0
- rclone_api-1.0.90/tests/archive/test_paramiko.py.disabled +326 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_diff.py +2 -2
- rclone_api-1.0.90/tests/test_mounted_ranged_download.py +151 -0
- rclone_api-1.0.90/tests/test_rclone_config.py +70 -0
- rclone_api-1.0.90/tests/test_s3.py +113 -0
- rclone_api-1.0.88/src/rclone_api/config.py +0 -8
- rclone_api-1.0.88/tests/test_serve_webdav.py +0 -108
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.aiderignore +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.pylintrc +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.vscode/launch.json +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.vscode/settings.json +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/LICENSE +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/MANIFEST.in +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/README.md +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/clean +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/install +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/lint +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/requirements.testing.txt +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/setup.cfg +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/process.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/test +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_copy.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_copy_files.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_group_files.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_ls.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_mount.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_remote_control.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_size_files.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tests/test_walk.py +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/tox.ini +0 -0
- {rclone_api-1.0.88 → rclone_api-1.0.90}/upload_package.sh +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: rclone_api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.90
|
|
4
4
|
Summary: rclone api in python
|
|
5
5
|
Home-page: https://github.com/zackees/rclone-api
|
|
6
|
-
Maintainer: Zachary Vorhies
|
|
7
6
|
License: BSD 3-Clause License
|
|
8
7
|
Keywords: template-python-cmd
|
|
9
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -12,8 +11,8 @@ Description-Content-Type: text/markdown
|
|
|
12
11
|
License-File: LICENSE
|
|
13
12
|
Requires-Dist: pyright>=1.1.393
|
|
14
13
|
Requires-Dist: python-dotenv>=1.0.0
|
|
14
|
+
Requires-Dist: boto3<=1.35.99,>=1.20.1
|
|
15
15
|
Dynamic: home-page
|
|
16
|
-
Dynamic: maintainer
|
|
17
16
|
|
|
18
17
|
# rclone-api
|
|
19
18
|
|
|
@@ -13,9 +13,15 @@ classifiers = ["Programming Language :: Python :: 3"]
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"pyright>=1.1.393",
|
|
15
15
|
"python-dotenv>=1.0.0",
|
|
16
|
+
|
|
17
|
+
# BOTO3 Library needs to be pinned to a specific version
|
|
18
|
+
# BackBlaze S3 fails with checksum header which it doesn't support after 1.35.99
|
|
19
|
+
# The 1.20.1 was the earliest one I checked that worked and is not the true lower bound.
|
|
20
|
+
"boto3>=1.20.1,<=1.35.99",
|
|
16
21
|
]
|
|
22
|
+
|
|
17
23
|
# Change this with the version number bump.
|
|
18
|
-
version = "1.0.
|
|
24
|
+
version = "1.0.90"
|
|
19
25
|
|
|
20
26
|
[tool.setuptools]
|
|
21
27
|
package-dir = {"" = "src"}
|
|
@@ -45,4 +51,5 @@ ignore_missing_imports = true
|
|
|
45
51
|
disable_error_code = ["import-untyped"]
|
|
46
52
|
|
|
47
53
|
[project.scripts]
|
|
48
|
-
rclone-api-listfiles = "rclone_api.cmd.list_files:main"
|
|
54
|
+
rclone-api-listfiles = "rclone_api.cmd.list_files:main"
|
|
55
|
+
rclone-api-copylarge-s3 = "rclone_api.cmd.copy_large_s3:main"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .completed_process import CompletedProcess
|
|
2
|
-
from .config import Config
|
|
2
|
+
from .config import Config, Parsed, Section
|
|
3
3
|
from .diff import DiffItem, DiffOption, DiffType
|
|
4
4
|
from .dir import Dir
|
|
5
5
|
from .dir_listing import DirListing
|
|
@@ -9,6 +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 .s3.types import MultiUploadResult
|
|
12
13
|
from .types import ListingOption, Order, SizeResult
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
@@ -30,4 +31,7 @@ __all__ = [
|
|
|
30
31
|
"Order",
|
|
31
32
|
"ListingOption",
|
|
32
33
|
"SizeResult",
|
|
34
|
+
"Parsed",
|
|
35
|
+
"Section",
|
|
36
|
+
"MultiUploadResult",
|
|
33
37
|
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rclone_api import MultiUploadResult, Rclone
|
|
6
|
+
|
|
7
|
+
_1MB = 1024 * 1024
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Args:
|
|
12
|
+
config_path: Path
|
|
13
|
+
src: str
|
|
14
|
+
dst: str
|
|
15
|
+
chunk_size_mb: int
|
|
16
|
+
read_concurrent_chunks: int
|
|
17
|
+
retries: int
|
|
18
|
+
save_state_json: Path
|
|
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(
|
|
33
|
+
"--config", help="Path to rclone config file", type=Path, required=True
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--chunk-size-mb", help="Chunk size in MB", type=int, default=256
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--read-concurrent-chunks",
|
|
40
|
+
help="Maximum number of chunks to read",
|
|
41
|
+
type=int,
|
|
42
|
+
default=4,
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument("--retries", help="Number of retries", type=int, default=3)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--resumable-json",
|
|
47
|
+
help="Path to resumable JSON file",
|
|
48
|
+
type=Path,
|
|
49
|
+
default="resume.json",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
out = Args(
|
|
54
|
+
config_path=Path(args.config),
|
|
55
|
+
src=args.src,
|
|
56
|
+
dst=args.dst,
|
|
57
|
+
chunk_size_mb=args.chunk_size_mb,
|
|
58
|
+
read_concurrent_chunks=args.read_concurrent_chunks,
|
|
59
|
+
retries=args.retries,
|
|
60
|
+
save_state_json=args.resumable_json,
|
|
61
|
+
)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> int:
|
|
66
|
+
"""Main entry point."""
|
|
67
|
+
args = _parse_args()
|
|
68
|
+
rclone = Rclone(rclone_conf=args.config_path)
|
|
69
|
+
rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
|
|
70
|
+
src=args.src,
|
|
71
|
+
dst=args.dst,
|
|
72
|
+
chunk_size=args.chunk_size_mb * _1MB,
|
|
73
|
+
concurrent_chunks=args.read_concurrent_chunks,
|
|
74
|
+
retries=args.retries,
|
|
75
|
+
save_state_json=args.save_state_json,
|
|
76
|
+
)
|
|
77
|
+
print(rslt)
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
import os
|
|
83
|
+
import sys
|
|
84
|
+
|
|
85
|
+
here = Path(__file__).parent
|
|
86
|
+
project_root = here.parent.parent.parent
|
|
87
|
+
print(f"project_root: {project_root}")
|
|
88
|
+
os.chdir(str(project_root))
|
|
89
|
+
cwd = Path(__file__).parent
|
|
90
|
+
print(f"cwd: {cwd}")
|
|
91
|
+
sys.argv.append("--config")
|
|
92
|
+
sys.argv.append("rclone.conf")
|
|
93
|
+
sys.argv.append(
|
|
94
|
+
"45061:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
|
|
95
|
+
)
|
|
96
|
+
sys.argv.append(
|
|
97
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
|
|
98
|
+
)
|
|
99
|
+
main()
|
|
@@ -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)
|
|
@@ -68,7 +68,10 @@ class TreeNode:
|
|
|
68
68
|
paths_reversed: list[str] = [self.name]
|
|
69
69
|
node: TreeNode | None = self
|
|
70
70
|
assert node is not None
|
|
71
|
-
while
|
|
71
|
+
while True:
|
|
72
|
+
node = node.parent
|
|
73
|
+
if node is None:
|
|
74
|
+
break
|
|
72
75
|
paths_reversed.append(node.name)
|
|
73
76
|
return "/".join(reversed(paths_reversed))
|
|
74
77
|
|