rclone-api 1.0.13__tar.gz → 1.0.14__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. rclone_api-1.0.14/.github/workflows/lint.yml +35 -0
  2. rclone_api-1.0.14/.github/workflows/push_macos.yml +32 -0
  3. rclone_api-1.0.14/.github/workflows/push_ubuntu.yml +32 -0
  4. rclone_api-1.0.14/.github/workflows/push_win.yml +34 -0
  5. {rclone_api-1.0.13/src/rclone_api.egg-info → rclone_api-1.0.14}/PKG-INFO +1 -1
  6. {rclone_api-1.0.13 → rclone_api-1.0.14}/pyproject.toml +1 -1
  7. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/__init__.py +1 -1
  8. rclone_api-1.0.14/src/rclone_api/config.py +8 -0
  9. rclone_api-1.0.14/src/rclone_api/convert.py +31 -0
  10. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/dir.py +16 -0
  11. rclone_api-1.0.14/src/rclone_api/dir_listing.py +40 -0
  12. rclone_api-1.0.13/src/rclone_api/config.py → rclone_api-1.0.14/src/rclone_api/exec.py +1 -1
  13. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/file.py +15 -0
  14. rclone_api-1.0.14/src/rclone_api/rclone.py +214 -0
  15. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/rpath.py +21 -10
  16. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/util.py +16 -4
  17. rclone_api-1.0.14/src/rclone_api/walk.py +70 -0
  18. {rclone_api-1.0.13 → rclone_api-1.0.14/src/rclone_api.egg-info}/PKG-INFO +1 -1
  19. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/SOURCES.txt +11 -2
  20. rclone_api-1.0.14/tests/test_copy.py +106 -0
  21. rclone_api-1.0.14/tests/test_is_synced.py +75 -0
  22. rclone_api-1.0.13/tests/test_simple.py → rclone_api-1.0.14/tests/test_ls.py +21 -23
  23. rclone_api-1.0.14/tests/test_remotes.py +70 -0
  24. rclone_api-1.0.14/tests/test_walk.py +68 -0
  25. rclone_api-1.0.13/src/rclone_api/dir_listing.py +0 -14
  26. rclone_api-1.0.13/src/rclone_api/rclone.py +0 -97
  27. rclone_api-1.0.13/src/rclone_api/types.py +0 -24
  28. rclone_api-1.0.13/src/rclone_api/walk.py +0 -87
  29. {rclone_api-1.0.13 → rclone_api-1.0.14}/.aiderignore +0 -0
  30. {rclone_api-1.0.13 → rclone_api-1.0.14}/.gitignore +0 -0
  31. {rclone_api-1.0.13 → rclone_api-1.0.14}/.pylintrc +0 -0
  32. {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/launch.json +0 -0
  33. {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/settings.json +0 -0
  34. {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/tasks.json +0 -0
  35. {rclone_api-1.0.13 → rclone_api-1.0.14}/LICENSE +0 -0
  36. {rclone_api-1.0.13 → rclone_api-1.0.14}/MANIFEST.in +0 -0
  37. {rclone_api-1.0.13 → rclone_api-1.0.14}/README.md +0 -0
  38. {rclone_api-1.0.13 → rclone_api-1.0.14}/clean +0 -0
  39. {rclone_api-1.0.13 → rclone_api-1.0.14}/install +0 -0
  40. {rclone_api-1.0.13 → rclone_api-1.0.14}/lint +0 -0
  41. {rclone_api-1.0.13 → rclone_api-1.0.14}/requirements.testing.txt +0 -0
  42. {rclone_api-1.0.13 → rclone_api-1.0.14}/setup.cfg +0 -0
  43. {rclone_api-1.0.13 → rclone_api-1.0.14}/setup.py +0 -0
  44. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/assets/example.txt +0 -0
  45. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/cli.py +0 -0
  46. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/remote.py +0 -0
  47. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  48. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/requires.txt +0 -0
  49. {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/top_level.txt +0 -0
  50. {rclone_api-1.0.13 → rclone_api-1.0.14}/test +0 -0
  51. {rclone_api-1.0.13 → rclone_api-1.0.14}/tox.ini +0 -0
  52. {rclone_api-1.0.13 → rclone_api-1.0.14}/upload_package.sh +0 -0
@@ -0,0 +1,35 @@
1
+ name: Linting
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ${{ matrix.os }}
8
+ strategy:
9
+ matrix:
10
+ python-version: [3.11]
11
+ os: [ubuntu-latest]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python ${{ matrix.python-version }}
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ - uses: actions/cache@v4
19
+ name: Configure pip caching
20
+ with:
21
+ path: ~/.cache/pip
22
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
23
+ restore-keys: |
24
+ ${{ runner.os }}-pip-
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v5
28
+
29
+ - name: Install
30
+ run: |
31
+ python -m pip install uv
32
+ ./install
33
+ - name: Run Linting
34
+ run: |
35
+ ./lint
@@ -0,0 +1,32 @@
1
+ name: MacOS_Tests
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ${{ matrix.os }}
8
+ strategy:
9
+ matrix:
10
+ python-version: [3.11]
11
+ os: [macos-latest]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python ${{ matrix.python-version }}
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ - uses: actions/cache@v4
19
+ name: Configure pip caching
20
+ with:
21
+ path: ~/.cache/pip
22
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
23
+ restore-keys: |
24
+ ${{ runner.os }}-pip-
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+ - name: Install
28
+ run: |
29
+ ./install
30
+ - name: Run Tests
31
+ run: |
32
+ ./test
@@ -0,0 +1,32 @@
1
+ name: Ubuntu_Tests
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ${{ matrix.os }}
8
+ strategy:
9
+ matrix:
10
+ python-version: [3.11]
11
+ os: [ubuntu-latest]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python ${{ matrix.python-version }}
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ - uses: actions/cache@v4
19
+ name: Configure pip caching
20
+ with:
21
+ path: ~/.cache/pip
22
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
23
+ restore-keys: |
24
+ ${{ runner.os }}-pip-
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+ - name: Install
28
+ run: |
29
+ ./install
30
+ - name: Run Tests
31
+ run: |
32
+ ./test
@@ -0,0 +1,34 @@
1
+ name: Win_Tests
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ${{ matrix.os }}
8
+ strategy:
9
+ matrix:
10
+ python-version: [3.11]
11
+ os: [windows-latest]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Python ${{ matrix.python-version }}
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python-version }}
18
+ - uses: actions/cache@v4
19
+ name: Configure pip caching
20
+ with:
21
+ path: ~/.cache/pip
22
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
23
+ restore-keys: |
24
+ ${{ runner.os }}-pip-
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+ - name: Install
28
+ run: |
29
+ ./install
30
+ shell: bash
31
+ - name: Run Tests
32
+ run: |
33
+ ./test
34
+ shell: bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.13
3
+ Version: 1.0.14
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "python-dotenv>=1.0.0"
15
15
  ]
16
16
  # Change this with the version number bump.
17
- version = "1.0.13"
17
+ version = "1.0.14"
18
18
 
19
19
  [tool.setuptools]
20
20
  package-dir = {"" = "src"}
@@ -1,9 +1,9 @@
1
+ from .config import Config
1
2
  from .dir import Dir
2
3
  from .dir_listing import DirListing
3
4
  from .file import File
4
5
  from .rclone import Rclone
5
6
  from .remote import Remote
6
7
  from .rpath import RPath
7
- from .types import Config
8
8
 
9
9
  __all__ = ["Rclone", "File", "Config", "Remote", "Dir", "RPath", "DirListing"]
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Config:
6
+ """Rclone configuration dataclass."""
7
+
8
+ text: str
@@ -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)}")
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import Generator
2
3
 
3
4
  from rclone_api.dir_listing import DirListing
@@ -12,6 +13,10 @@ class Dir:
12
13
  def remote(self) -> Remote:
13
14
  return self.path.remote
14
15
 
16
+ @property
17
+ def name(self) -> str:
18
+ return self.path.name
19
+
15
20
  def __init__(self, path: RPath | Remote) -> None:
16
21
  """Initialize Dir with either an RPath or Remote.
17
22
 
@@ -33,6 +38,8 @@ class Dir:
33
38
  self.path.set_rclone(path.rclone)
34
39
  else:
35
40
  self.path = path
41
+ # self.path.set_rclone(self.path.remote.rclone)
42
+ assert self.path.rclone is not None
36
43
 
37
44
  def ls(self, max_depth: int = 0) -> DirListing:
38
45
  """List files and directories in the given path."""
@@ -47,5 +54,14 @@ class Dir:
47
54
  assert self.path.rclone is not None
48
55
  return walk(self, max_depth=max_depth)
49
56
 
57
+ def to_json(self) -> dict:
58
+ """Convert the Dir to a JSON serializable dictionary."""
59
+ return self.path.to_json()
60
+
50
61
  def __str__(self) -> str:
51
62
  return str(self.path)
63
+
64
+ def __repr__(self) -> str:
65
+ data = self.path.to_json()
66
+ data_str = json.dumps(data)
67
+ return data_str
@@ -0,0 +1,40 @@
1
+ import json
2
+
3
+ from rclone_api.rpath import RPath
4
+
5
+
6
+ class DirListing:
7
+ """Remote file dataclass."""
8
+
9
+ def __init__(self, dirs_and_files: list[RPath]) -> None:
10
+ from rclone_api.dir import Dir
11
+ from rclone_api.file import File
12
+
13
+ self.dirs: list[Dir] = [Dir(d) for d in dirs_and_files if d.is_dir]
14
+ self.files: list[File] = [File(f) for f in dirs_and_files if not f.is_dir]
15
+
16
+ def __str__(self) -> str:
17
+ n_files = len(self.files)
18
+ n_dirs = len(self.dirs)
19
+ msg = f"Files: {n_files}\n"
20
+ if n_files > 0:
21
+ for f in self.files:
22
+ msg += f" {f}\n"
23
+ msg += f"Dirs: {n_dirs}\n"
24
+ if n_dirs > 0:
25
+ for d in self.dirs:
26
+ msg += f" {d}\n"
27
+ return msg
28
+
29
+ def __repr__(self) -> str:
30
+ dirs: list = []
31
+ files: list = []
32
+ for d in self.dirs:
33
+ dirs.append(d.path.to_json())
34
+ for f in self.files:
35
+ files.append(f.path.to_json())
36
+ json_obj = {
37
+ "dirs": dirs,
38
+ "files": files,
39
+ }
40
+ return json.dumps(json_obj, indent=2)
@@ -2,7 +2,7 @@ import subprocess
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
4
 
5
- from rclone_api.types import Config
5
+ from rclone_api.config import Config
6
6
 
7
7
 
8
8
  @dataclass
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from rclone_api.rpath import RPath
2
4
 
3
5
 
@@ -10,6 +12,10 @@ class File:
10
12
  ) -> None:
11
13
  self.path = path
12
14
 
15
+ @property
16
+ def name(self) -> str:
17
+ return self.path.name
18
+
13
19
  def read_text(self) -> str:
14
20
  """Read the file contents as bytes.
15
21
 
@@ -28,5 +34,14 @@ class File:
28
34
  result = self.path.rclone._run(["cat", self.path.path])
29
35
  return result.stdout
30
36
 
37
+ def to_json(self) -> dict:
38
+ """Convert the File to a JSON serializable dictionary."""
39
+ return self.path.to_json()
40
+
31
41
  def __str__(self) -> str:
32
42
  return str(self.path)
43
+
44
+ def __repr__(self) -> str:
45
+ data = self.path.to_json()
46
+ data_str = json.dumps(data)
47
+ return data_str
@@ -0,0 +1,214 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import subprocess
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from fnmatch import fnmatch
8
+ from pathlib import Path
9
+ from typing import Generator
10
+
11
+ from rclone_api import Dir
12
+ from rclone_api.config import Config
13
+ from rclone_api.convert import convert_to_filestr_list, convert_to_str
14
+ from rclone_api.dir_listing import DirListing
15
+ from rclone_api.exec import RcloneExec
16
+ from rclone_api.file import File
17
+ from rclone_api.remote import Remote
18
+ from rclone_api.rpath import RPath
19
+ from rclone_api.util import get_rclone_exe, to_path
20
+ from rclone_api.walk import walk
21
+
22
+
23
+ class Rclone:
24
+ def __init__(
25
+ self, rclone_conf: Path | Config, rclone_exe: Path | None = None
26
+ ) -> None:
27
+ if isinstance(rclone_conf, Path):
28
+ if not rclone_conf.exists():
29
+ raise ValueError(f"Rclone config file not found: {rclone_conf}")
30
+ self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
31
+
32
+ def _run(self, cmd: list[str]) -> subprocess.CompletedProcess:
33
+ return self._exec.execute(cmd)
34
+
35
+ def ls(
36
+ self,
37
+ path: Dir | Remote | str,
38
+ max_depth: int | None = None,
39
+ glob: str | None = None,
40
+ ) -> DirListing:
41
+ """List files in the given path.
42
+
43
+ Args:
44
+ path: Remote path or Remote object to list
45
+ max_depth: Maximum recursion depth (0 means no recursion)
46
+
47
+ Returns:
48
+ List of File objects found at the path
49
+ """
50
+
51
+ if isinstance(path, str):
52
+ path = Dir(
53
+ to_path(path, self)
54
+ ) # assume it's a directory if ls is being called.
55
+
56
+ cmd = ["lsjson"]
57
+ if max_depth is not None:
58
+ cmd.append("--recursive")
59
+ if max_depth > -1:
60
+ cmd.append("--max-depth")
61
+ cmd.append(str(max_depth))
62
+ cmd.append(str(path))
63
+ remote = path.remote if isinstance(path, Dir) else path
64
+ assert isinstance(remote, Remote)
65
+
66
+ cp = self._run(cmd)
67
+ text = cp.stdout
68
+ parent_path: str | None = None
69
+ if isinstance(path, Dir):
70
+ parent_path = path.path.path
71
+ paths: list[RPath] = RPath.from_json_str(text, remote, parent_path=parent_path)
72
+ # print(parent_path)
73
+ for o in paths:
74
+ o.set_rclone(self)
75
+
76
+ # do we have a glob pattern?
77
+ if glob is not None:
78
+ paths = [p for p in paths if fnmatch(p.path, glob)]
79
+ return DirListing(paths)
80
+
81
+ def listremotes(self) -> list[Remote]:
82
+ cmd = ["listremotes"]
83
+ cp = self._run(cmd)
84
+ text: str = cp.stdout
85
+ tmp = text.splitlines()
86
+ tmp = [t.strip() for t in tmp]
87
+ # strip out ":" from the end
88
+ tmp = [t.replace(":", "") for t in tmp]
89
+ out = [Remote(name=t, rclone=self) for t in tmp]
90
+ return out
91
+
92
+ def walk(
93
+ self, path: Dir | Remote | str, max_depth: int = -1
94
+ ) -> Generator[DirListing, None, None]:
95
+ """Walk through the given path recursively.
96
+
97
+ Args:
98
+ path: Remote path or Remote object to walk through
99
+ max_depth: Maximum depth to traverse (-1 for unlimited)
100
+
101
+ Yields:
102
+ DirListing: Directory listing for each directory encountered
103
+ """
104
+ if isinstance(path, Dir):
105
+ # Create a Remote object for the path
106
+ remote = path.remote
107
+ rpath = RPath(
108
+ remote=remote,
109
+ path=path.path.path,
110
+ name=path.path.name,
111
+ size=0,
112
+ mime_type="inode/directory",
113
+ mod_time="",
114
+ is_dir=True,
115
+ )
116
+ rpath.set_rclone(self)
117
+ dir_obj = Dir(rpath)
118
+ elif isinstance(path, str):
119
+ dir_obj = Dir(to_path(path, self))
120
+ elif isinstance(path, Remote):
121
+ dir_obj = Dir(path)
122
+ else:
123
+ assert f"Invalid type for path: {type(path)}"
124
+
125
+ yield from walk(dir_obj, max_depth=max_depth)
126
+
127
+ def copyfile(self, src: File | str, dst: File | str) -> None:
128
+ """Copy a single file from source to destination.
129
+
130
+ Args:
131
+ src: Source file path (including remote if applicable)
132
+ dst: Destination file path (including remote if applicable)
133
+
134
+ Raises:
135
+ subprocess.CalledProcessError: If the copy operation fails
136
+ """
137
+ src = src if isinstance(src, str) else str(src.path)
138
+ dst = dst if isinstance(dst, str) else str(dst.path)
139
+ cmd_list: list[str] = ["copyto", src, dst]
140
+ self._run(cmd_list)
141
+
142
+ def copyfiles(self, filelist: dict[File, File] | dict[str, str]) -> None:
143
+ """Copy multiple files from source to destination.
144
+
145
+ Warning - slow.
146
+
147
+ Args:
148
+ payload: Dictionary of source and destination file paths
149
+ """
150
+ str_dict: dict[str, str] = {}
151
+ for src, dst in filelist.items():
152
+ src = src if isinstance(src, str) else str(src.path)
153
+ dst = dst if isinstance(dst, str) else str(dst.path)
154
+ str_dict[src] = dst
155
+
156
+ with ThreadPoolExecutor(max_workers=64) as executor:
157
+ for src, dst in str_dict.items(): # warning - slow
158
+ cmd_list: list[str] = ["copyto", src, dst]
159
+ # self._run(cmd_list)
160
+ executor.submit(self._run, cmd_list)
161
+
162
+ def copy(self, src: Dir, dst: Dir) -> None:
163
+ """Copy files from source to destination.
164
+
165
+ Args:
166
+ src: Source directory
167
+ dst: Destination directory
168
+ """
169
+ src_dir = src.path.path
170
+ dst_dir = dst.path.path
171
+ cmd_list: list[str] = ["copy", src_dir, dst_dir]
172
+ self._run(cmd_list)
173
+
174
+ def purge(self, path: Dir | str) -> None:
175
+ """Purge a directory"""
176
+ # path should always be a string
177
+ path = path if isinstance(path, str) else str(path.path)
178
+ cmd_list: list[str] = ["purge", str(path)]
179
+ self._run(cmd_list)
180
+
181
+ def deletefiles(self, files: str | File | list[str] | list[File]) -> None:
182
+ """Delete a directory"""
183
+ payload: list[str] = convert_to_filestr_list(files)
184
+ cmd_list: list[str] = ["delete"] + payload
185
+ self._run(cmd_list)
186
+
187
+ def exists(self, path: Dir | Remote | str | File) -> bool:
188
+ """Check if a file or directory exists."""
189
+ arg: str = convert_to_str(path)
190
+ assert isinstance(arg, str)
191
+ try:
192
+ self.ls(arg)
193
+ return True
194
+ except subprocess.CalledProcessError:
195
+ return False
196
+
197
+ def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
198
+ """Check if two directories are in sync."""
199
+ src = convert_to_str(src)
200
+ dst = convert_to_str(dst)
201
+ cmd_list: list[str] = ["check", str(src), str(dst)]
202
+ try:
203
+ self._run(cmd_list)
204
+ return True
205
+ except subprocess.CalledProcessError:
206
+ return False
207
+
208
+ def copy_dir(self, src: str | Dir, dst: str | Dir) -> None:
209
+ """Copy a directory from source to destination."""
210
+ # convert src to str, also dst
211
+ src = convert_to_str(src)
212
+ dst = convert_to_str(dst)
213
+ cmd_list: list[str] = ["copy", src, dst]
214
+ self._run(cmd_list)
@@ -19,9 +19,6 @@ class RPath:
19
19
  ) -> None:
20
20
  from rclone_api.rclone import Rclone
21
21
 
22
- if "dst:" in path:
23
- raise ValueError(f"Invalid path: {path}")
24
-
25
22
  self.remote = remote
26
23
  self.path = path
27
24
  self.name = name
@@ -39,11 +36,16 @@ class RPath:
39
36
  self.rclone = rclone
40
37
 
41
38
  @staticmethod
42
- def from_dict(data: dict, remote: Remote) -> "RPath":
39
+ def from_dict(
40
+ data: dict, remote: Remote, parent_path: str | None = None
41
+ ) -> "RPath":
43
42
  """Create a File from a dictionary."""
43
+ path = data["Path"]
44
+ if parent_path is not None:
45
+ path = f"{parent_path}/{path}"
44
46
  return RPath(
45
47
  remote,
46
- data["Path"],
48
+ path,
47
49
  data["Name"],
48
50
  data["Size"],
49
51
  data["MimeType"],
@@ -53,21 +55,25 @@ class RPath:
53
55
  )
54
56
 
55
57
  @staticmethod
56
- def from_array(data: list[dict], remote: Remote) -> list["RPath"]:
58
+ def from_array(
59
+ data: list[dict], remote: Remote, parent_path: str | None = None
60
+ ) -> list["RPath"]:
57
61
  """Create a File from a dictionary."""
58
62
  out: list[RPath] = []
59
63
  for d in data:
60
- file: RPath = RPath.from_dict(d, remote)
64
+ file: RPath = RPath.from_dict(d, remote, parent_path)
61
65
  out.append(file)
62
66
  return out
63
67
 
64
68
  @staticmethod
65
- def from_json_str(json_str: str, remote: Remote) -> list["RPath"]:
69
+ def from_json_str(
70
+ json_str: str, remote: Remote, parent_path: str | None = None
71
+ ) -> list["RPath"]:
66
72
  """Create a File from a JSON string."""
67
73
  json_obj = json.loads(json_str)
68
74
  if isinstance(json_obj, dict):
69
- return [RPath.from_dict(json_obj, remote)]
70
- return RPath.from_array(json_obj, remote)
75
+ return [RPath.from_dict(json_obj, remote, parent_path)]
76
+ return RPath.from_array(json_obj, remote, parent_path)
71
77
 
72
78
  def to_json(self) -> dict:
73
79
  return {
@@ -82,3 +88,8 @@ class RPath:
82
88
 
83
89
  def __str__(self) -> str:
84
90
  return f"{self.remote.name}:{self.path}"
91
+
92
+ def __repr__(self):
93
+ data = self.to_json()
94
+ data["Remote"] = self.remote.name
95
+ return json.dumps(data)
@@ -5,10 +5,10 @@ from pathlib import Path
5
5
  from tempfile import TemporaryDirectory
6
6
  from typing import Any
7
7
 
8
+ from rclone_api.config import Config
8
9
  from rclone_api.dir import Dir
9
10
  from rclone_api.remote import Remote
10
11
  from rclone_api.rpath import RPath
11
- from rclone_api.types import Config
12
12
 
13
13
  # from .rclone import Rclone
14
14
 
@@ -25,7 +25,7 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
25
25
  remote_name = parts[0]
26
26
  path = ":".join(parts[1:])
27
27
  remote = Remote(name=remote_name, rclone=rclone)
28
- return RPath(
28
+ out = RPath(
29
29
  remote=remote,
30
30
  path=path,
31
31
  name="",
@@ -34,10 +34,12 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
34
34
  mod_time="",
35
35
  is_dir=True,
36
36
  )
37
+ out.set_rclone(rclone)
38
+ return out
37
39
  elif isinstance(item, Dir):
38
40
  return item.path
39
41
  elif isinstance(item, Remote):
40
- return RPath(
42
+ out = RPath(
41
43
  remote=item,
42
44
  path=str(item),
43
45
  name=str(item),
@@ -46,6 +48,8 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
46
48
  mod_time="",
47
49
  is_dir=True,
48
50
  )
51
+ out.set_rclone(rclone)
52
+ return out
49
53
  else:
50
54
  raise ValueError(f"Invalid type for item: {type(item)}")
51
55
 
@@ -90,8 +94,16 @@ def rclone_execute(
90
94
  cmd_str = subprocess.list2cmdline(cmd)
91
95
  print(f"Running: {cmd_str}")
92
96
  cp = subprocess.run(
93
- cmd, capture_output=True, encoding="utf-8", check=True, shell=False
97
+ cmd, capture_output=True, encoding="utf-8", check=False, shell=False
94
98
  )
99
+ if cp.returncode != 0:
100
+ cmd_str = subprocess.list2cmdline(cmd)
101
+ print(
102
+ f"Error running: {cmd_str}, returncode: {cp.returncode}\n{cp.stdout}\n{cp.stderr}"
103
+ )
104
+ raise subprocess.CalledProcessError(
105
+ cp.returncode, cmd, cp.stdout, cp.stderr
106
+ )
95
107
  return cp
96
108
  finally:
97
109
  if tempdir: