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.
- rclone_api/__init__.py +951 -0
- rclone_api/assets/example.txt +1 -0
- rclone_api/cli.py +15 -0
- rclone_api/cmd/analyze.py +51 -0
- rclone_api/cmd/copy_large_s3.py +111 -0
- rclone_api/cmd/copy_large_s3_finish.py +81 -0
- rclone_api/cmd/list_files.py +27 -0
- rclone_api/cmd/save_to_db.py +77 -0
- rclone_api/completed_process.py +60 -0
- rclone_api/config.py +87 -0
- rclone_api/convert.py +31 -0
- rclone_api/db/__init__.py +3 -0
- rclone_api/db/db.py +277 -0
- rclone_api/db/models.py +57 -0
- rclone_api/deprecated.py +24 -0
- rclone_api/detail/copy_file_parts_resumable.py +42 -0
- rclone_api/detail/walk.py +116 -0
- rclone_api/diff.py +164 -0
- rclone_api/dir.py +113 -0
- rclone_api/dir_listing.py +66 -0
- rclone_api/exec.py +40 -0
- rclone_api/experimental/flags.py +89 -0
- rclone_api/experimental/flags_base.py +58 -0
- rclone_api/file.py +205 -0
- rclone_api/file_item.py +68 -0
- rclone_api/file_part.py +198 -0
- rclone_api/file_stream.py +52 -0
- rclone_api/filelist.py +30 -0
- rclone_api/group_files.py +256 -0
- rclone_api/http_server.py +244 -0
- rclone_api/install.py +95 -0
- rclone_api/log.py +44 -0
- rclone_api/mount.py +55 -0
- rclone_api/mount_util.py +247 -0
- rclone_api/process.py +187 -0
- rclone_api/rclone_impl.py +1285 -0
- rclone_api/remote.py +21 -0
- rclone_api/rpath.py +102 -0
- rclone_api/s3/api.py +109 -0
- rclone_api/s3/basic_ops.py +61 -0
- rclone_api/s3/chunk_task.py +187 -0
- rclone_api/s3/create.py +107 -0
- rclone_api/s3/multipart/file_info.py +7 -0
- rclone_api/s3/multipart/finished_piece.py +69 -0
- rclone_api/s3/multipart/info_json.py +239 -0
- rclone_api/s3/multipart/merge_state.py +147 -0
- rclone_api/s3/multipart/upload_info.py +62 -0
- rclone_api/s3/multipart/upload_parts_inline.py +356 -0
- rclone_api/s3/multipart/upload_parts_resumable.py +304 -0
- rclone_api/s3/multipart/upload_parts_server_side_merge.py +546 -0
- rclone_api/s3/multipart/upload_state.py +165 -0
- rclone_api/s3/types.py +67 -0
- rclone_api/scan_missing_folders.py +153 -0
- rclone_api/types.py +402 -0
- rclone_api/util.py +324 -0
- rclone_api-1.5.8.dist-info/LICENSE +21 -0
- rclone_api-1.5.8.dist-info/METADATA +969 -0
- rclone_api-1.5.8.dist-info/RECORD +61 -0
- rclone_api-1.5.8.dist-info/WHEEL +5 -0
- rclone_api-1.5.8.dist-info/entry_points.txt +5 -0
- 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,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)}")
|