rclone-api 1.4.32__py2.py3-none-any.whl → 1.5.0__py2.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/db/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- from .db import DB
2
-
3
- __all__ = ["DB"]
1
+ from .db import DB
2
+
3
+ __all__ = ["DB"]
rclone_api/detail/walk.py CHANGED
@@ -1,116 +1,116 @@
1
- import random
2
- from queue import Queue
3
- from threading import Thread
4
- from typing import Generator
5
-
6
- from rclone_api import Dir
7
- from rclone_api.dir_listing import DirListing
8
- from rclone_api.remote import Remote
9
- from rclone_api.types import Order
10
-
11
- _MAX_OUT_QUEUE_SIZE = 50
12
-
13
-
14
- def walk_runner_breadth_first(
15
- dir: Dir,
16
- max_depth: int,
17
- out_queue: Queue[DirListing | None],
18
- order: Order = Order.NORMAL,
19
- ) -> None:
20
- queue: Queue[Dir] = Queue()
21
- queue.put(dir)
22
- try:
23
- while not queue.empty():
24
- current_dir = queue.get()
25
- dirlisting = current_dir.ls(max_depth=0, order=order)
26
- out_queue.put(dirlisting)
27
- dirs = dirlisting.dirs
28
-
29
- if max_depth != 0 and len(dirs) > 0:
30
- for child in dirs:
31
- queue.put(child)
32
- if max_depth < 0:
33
- continue
34
- if max_depth > 0:
35
- max_depth -= 1
36
- out_queue.put(None)
37
- except KeyboardInterrupt:
38
- import _thread
39
-
40
- out_queue.put(None)
41
-
42
- _thread.interrupt_main()
43
-
44
-
45
- def walk_runner_depth_first(
46
- dir: Dir,
47
- max_depth: int,
48
- out_queue: Queue[DirListing | None],
49
- order: Order = Order.NORMAL,
50
- ) -> None:
51
- try:
52
- stack = [(dir, max_depth)]
53
- while stack:
54
- current_dir, depth = stack.pop()
55
- dirlisting = current_dir.ls()
56
- if order == Order.REVERSE:
57
- dirlisting.dirs.reverse()
58
- if order == Order.RANDOM:
59
-
60
- random.shuffle(dirlisting.dirs)
61
- if depth != 0:
62
- for subdir in dirlisting.dirs: # Process deeper directories first
63
- # stack.append((child, depth - 1 if depth > 0 else depth))
64
- next_depth = depth - 1 if depth > 0 else depth
65
- walk_runner_depth_first(subdir, next_depth, out_queue, order=order)
66
- out_queue.put(dirlisting)
67
- out_queue.put(None)
68
- except KeyboardInterrupt:
69
- import _thread
70
-
71
- out_queue.put(None)
72
- _thread.interrupt_main()
73
-
74
-
75
- def walk(
76
- dir: Dir | Remote,
77
- breadth_first: bool,
78
- max_depth: int = -1,
79
- order: Order = Order.NORMAL,
80
- ) -> Generator[DirListing, None, None]:
81
- """Walk through the given directory recursively.
82
-
83
- Args:
84
- dir: Directory or Remote to walk through
85
- max_depth: Maximum depth to traverse (-1 for unlimited)
86
-
87
- Yields:
88
- DirListing: Directory listing for each directory encountered
89
- """
90
- try:
91
- # Convert Remote to Dir if needed
92
- if isinstance(dir, Remote):
93
- dir = Dir(dir)
94
- out_queue: Queue[DirListing | None] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
95
-
96
- def _task() -> None:
97
- if breadth_first:
98
- walk_runner_breadth_first(dir, max_depth, out_queue, order)
99
- else:
100
- walk_runner_depth_first(dir, max_depth, out_queue, order)
101
-
102
- # Start worker thread
103
- worker = Thread(
104
- target=_task,
105
- daemon=True,
106
- )
107
- worker.start()
108
-
109
- while dirlisting := out_queue.get():
110
- if dirlisting is None:
111
- break
112
- yield dirlisting
113
-
114
- worker.join()
115
- except KeyboardInterrupt:
116
- pass
1
+ import random
2
+ from queue import Queue
3
+ from threading import Thread
4
+ from typing import Generator
5
+
6
+ from rclone_api import Dir
7
+ from rclone_api.dir_listing import DirListing
8
+ from rclone_api.remote import Remote
9
+ from rclone_api.types import Order
10
+
11
+ _MAX_OUT_QUEUE_SIZE = 50
12
+
13
+
14
+ def walk_runner_breadth_first(
15
+ dir: Dir,
16
+ max_depth: int,
17
+ out_queue: Queue[DirListing | None],
18
+ order: Order = Order.NORMAL,
19
+ ) -> None:
20
+ queue: Queue[Dir] = Queue()
21
+ queue.put(dir)
22
+ try:
23
+ while not queue.empty():
24
+ current_dir = queue.get()
25
+ dirlisting = current_dir.ls(max_depth=0, order=order)
26
+ out_queue.put(dirlisting)
27
+ dirs = dirlisting.dirs
28
+
29
+ if max_depth != 0 and len(dirs) > 0:
30
+ for child in dirs:
31
+ queue.put(child)
32
+ if max_depth < 0:
33
+ continue
34
+ if max_depth > 0:
35
+ max_depth -= 1
36
+ out_queue.put(None)
37
+ except KeyboardInterrupt:
38
+ import _thread
39
+
40
+ out_queue.put(None)
41
+
42
+ _thread.interrupt_main()
43
+
44
+
45
+ def walk_runner_depth_first(
46
+ dir: Dir,
47
+ max_depth: int,
48
+ out_queue: Queue[DirListing | None],
49
+ order: Order = Order.NORMAL,
50
+ ) -> None:
51
+ try:
52
+ stack = [(dir, max_depth)]
53
+ while stack:
54
+ current_dir, depth = stack.pop()
55
+ dirlisting = current_dir.ls()
56
+ if order == Order.REVERSE:
57
+ dirlisting.dirs.reverse()
58
+ if order == Order.RANDOM:
59
+
60
+ random.shuffle(dirlisting.dirs)
61
+ if depth != 0:
62
+ for subdir in dirlisting.dirs: # Process deeper directories first
63
+ # stack.append((child, depth - 1 if depth > 0 else depth))
64
+ next_depth = depth - 1 if depth > 0 else depth
65
+ walk_runner_depth_first(subdir, next_depth, out_queue, order=order)
66
+ out_queue.put(dirlisting)
67
+ out_queue.put(None)
68
+ except KeyboardInterrupt:
69
+ import _thread
70
+
71
+ out_queue.put(None)
72
+ _thread.interrupt_main()
73
+
74
+
75
+ def walk(
76
+ dir: Dir | Remote,
77
+ breadth_first: bool,
78
+ max_depth: int = -1,
79
+ order: Order = Order.NORMAL,
80
+ ) -> Generator[DirListing, None, None]:
81
+ """Walk through the given directory recursively.
82
+
83
+ Args:
84
+ dir: Directory or Remote to walk through
85
+ max_depth: Maximum depth to traverse (-1 for unlimited)
86
+
87
+ Yields:
88
+ DirListing: Directory listing for each directory encountered
89
+ """
90
+ try:
91
+ # Convert Remote to Dir if needed
92
+ if isinstance(dir, Remote):
93
+ dir = Dir(dir)
94
+ out_queue: Queue[DirListing | None] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
95
+
96
+ def _task() -> None:
97
+ if breadth_first:
98
+ walk_runner_breadth_first(dir, max_depth, out_queue, order)
99
+ else:
100
+ walk_runner_depth_first(dir, max_depth, out_queue, order)
101
+
102
+ # Start worker thread
103
+ worker = Thread(
104
+ target=_task,
105
+ daemon=True,
106
+ )
107
+ worker.start()
108
+
109
+ while dirlisting := out_queue.get():
110
+ if dirlisting is None:
111
+ break
112
+ yield dirlisting
113
+
114
+ worker.join()
115
+ except KeyboardInterrupt:
116
+ pass
rclone_api/dir.py CHANGED
@@ -1,113 +1,113 @@
1
- import json
2
- from pathlib import Path
3
- from typing import Generator
4
-
5
- from rclone_api.dir_listing import DirListing
6
- from rclone_api.remote import Remote
7
- from rclone_api.rpath import RPath
8
- from rclone_api.types import ListingOption, Order
9
-
10
-
11
- class Dir:
12
- """Remote file dataclass."""
13
-
14
- @property
15
- def remote(self) -> Remote:
16
- return self.path.remote
17
-
18
- @property
19
- def name(self) -> str:
20
- return self.path.name
21
-
22
- def __init__(self, path: RPath | Remote) -> None:
23
- """Initialize Dir with either an RPath or Remote.
24
-
25
- Args:
26
- path: Either an RPath object or a Remote object
27
- """
28
- if isinstance(path, Remote):
29
- # Need to create an RPath for the Remote's root
30
- self.path = RPath(
31
- remote=path,
32
- path=str(path),
33
- name=str(path),
34
- size=0,
35
- mime_type="inode/directory",
36
- mod_time="",
37
- is_dir=True,
38
- )
39
- # Ensure the RPath has the same rclone instance as the Remote
40
- self.path.set_rclone(path.rclone)
41
- else:
42
- self.path = path
43
- # self.path.set_rclone(self.path.remote.rclone)
44
- assert self.path.rclone is not None
45
-
46
- def ls(
47
- self,
48
- max_depth: int | None = None,
49
- glob: str | None = None,
50
- order: Order = Order.NORMAL,
51
- listing_option: ListingOption = ListingOption.ALL,
52
- ) -> DirListing:
53
- """List files and directories in the given path."""
54
- assert self.path.rclone is not None
55
- dir = Dir(self.path)
56
- return self.path.rclone.ls(
57
- dir,
58
- max_depth=max_depth,
59
- glob=glob,
60
- order=order,
61
- listing_option=listing_option,
62
- )
63
-
64
- def relative_to(self, other: "Dir") -> str:
65
- """Return the relative path to the other directory."""
66
- self_path = Path(self.path.path)
67
- other_path = Path(other.path.path)
68
- rel_path = self_path.relative_to(other_path)
69
- return str(rel_path.as_posix())
70
-
71
- def walk(
72
- self, breadth_first: bool, max_depth: int = -1
73
- ) -> Generator[DirListing, None, None]:
74
- """List files and directories in the given path."""
75
- from rclone_api.detail.walk import walk
76
-
77
- assert self.path.rclone is not None
78
- return walk(self, breadth_first=breadth_first, max_depth=max_depth)
79
-
80
- def to_json(self) -> dict:
81
- """Convert the Dir to a JSON serializable dictionary."""
82
- return self.path.to_json()
83
-
84
- def __str__(self) -> str:
85
- return str(self.path)
86
-
87
- def __repr__(self) -> str:
88
- data = self.path.to_json()
89
- data_str = json.dumps(data)
90
- return data_str
91
-
92
- def to_string(self, include_remote: bool = True) -> str:
93
- """Convert the File to a string."""
94
- out = str(self.path)
95
- if not include_remote:
96
- _, out = out.split(":", 1)
97
- return out
98
-
99
- # / operator
100
- def __truediv__(self, other: str) -> "Dir":
101
- """Join the current path with another path."""
102
- path = Path(self.path.path) / other
103
- rpath = RPath(
104
- self.path.remote,
105
- str(path.as_posix()),
106
- name=other,
107
- size=0,
108
- mime_type="inode/directory",
109
- mod_time="",
110
- is_dir=True,
111
- )
112
- rpath.set_rclone(self.path.rclone)
113
- return Dir(rpath)
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Generator
4
+
5
+ from rclone_api.dir_listing import DirListing
6
+ from rclone_api.remote import Remote
7
+ from rclone_api.rpath import RPath
8
+ from rclone_api.types import ListingOption, Order
9
+
10
+
11
+ class Dir:
12
+ """Remote file dataclass."""
13
+
14
+ @property
15
+ def remote(self) -> Remote:
16
+ return self.path.remote
17
+
18
+ @property
19
+ def name(self) -> str:
20
+ return self.path.name
21
+
22
+ def __init__(self, path: RPath | Remote) -> None:
23
+ """Initialize Dir with either an RPath or Remote.
24
+
25
+ Args:
26
+ path: Either an RPath object or a Remote object
27
+ """
28
+ if isinstance(path, Remote):
29
+ # Need to create an RPath for the Remote's root
30
+ self.path = RPath(
31
+ remote=path,
32
+ path=str(path),
33
+ name=str(path),
34
+ size=0,
35
+ mime_type="inode/directory",
36
+ mod_time="",
37
+ is_dir=True,
38
+ )
39
+ # Ensure the RPath has the same rclone instance as the Remote
40
+ self.path.set_rclone(path.rclone)
41
+ else:
42
+ self.path = path
43
+ # self.path.set_rclone(self.path.remote.rclone)
44
+ assert self.path.rclone is not None
45
+
46
+ def ls(
47
+ self,
48
+ max_depth: int | None = None,
49
+ glob: str | None = None,
50
+ order: Order = Order.NORMAL,
51
+ listing_option: ListingOption = ListingOption.ALL,
52
+ ) -> DirListing:
53
+ """List files and directories in the given path."""
54
+ assert self.path.rclone is not None
55
+ dir = Dir(self.path)
56
+ return self.path.rclone.ls(
57
+ dir,
58
+ max_depth=max_depth,
59
+ glob=glob,
60
+ order=order,
61
+ listing_option=listing_option,
62
+ )
63
+
64
+ def relative_to(self, other: "Dir") -> str:
65
+ """Return the relative path to the other directory."""
66
+ self_path = Path(self.path.path)
67
+ other_path = Path(other.path.path)
68
+ rel_path = self_path.relative_to(other_path)
69
+ return str(rel_path.as_posix())
70
+
71
+ def walk(
72
+ self, breadth_first: bool, max_depth: int = -1
73
+ ) -> Generator[DirListing, None, None]:
74
+ """List files and directories in the given path."""
75
+ from rclone_api.detail.walk import walk
76
+
77
+ assert self.path.rclone is not None
78
+ return walk(self, breadth_first=breadth_first, max_depth=max_depth)
79
+
80
+ def to_json(self) -> dict:
81
+ """Convert the Dir to a JSON serializable dictionary."""
82
+ return self.path.to_json()
83
+
84
+ def __str__(self) -> str:
85
+ return str(self.path)
86
+
87
+ def __repr__(self) -> str:
88
+ data = self.path.to_json()
89
+ data_str = json.dumps(data)
90
+ return data_str
91
+
92
+ def to_string(self, include_remote: bool = True) -> str:
93
+ """Convert the File to a string."""
94
+ out = str(self.path)
95
+ if not include_remote:
96
+ _, out = out.split(":", 1)
97
+ return out
98
+
99
+ # / operator
100
+ def __truediv__(self, other: str) -> "Dir":
101
+ """Join the current path with another path."""
102
+ path = Path(self.path.path) / other
103
+ rpath = RPath(
104
+ self.path.remote,
105
+ str(path.as_posix()),
106
+ name=other,
107
+ size=0,
108
+ mime_type="inode/directory",
109
+ mod_time="",
110
+ is_dir=True,
111
+ )
112
+ rpath.set_rclone(self.path.rclone)
113
+ return Dir(rpath)
rclone_api/install.py ADDED
@@ -0,0 +1,90 @@
1
+ import os
2
+ import platform
3
+ import shutil
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+ from warnings import warn
7
+
8
+ from download import download
9
+
10
+ URL_WINDOWS = "https://downloads.rclone.org/rclone-current-windows-amd64.zip"
11
+ URL_LINUX = "https://downloads.rclone.org/rclone-current-linux-amd64.zip"
12
+ URL_MACOS_ARM = "https://downloads.rclone.org/rclone-current-osx-arm64.zip"
13
+ URL_MACOS_X86 = "https://downloads.rclone.org/rclone-current-osx-amd64.zip"
14
+
15
+
16
+ def rclone_download_url() -> str:
17
+ system = platform.system()
18
+ if system == "Windows":
19
+ return URL_WINDOWS
20
+ elif system == "Linux":
21
+ return URL_LINUX
22
+ elif system == "Darwin":
23
+ arch = platform.machine()
24
+ if "x86" in arch:
25
+ return URL_MACOS_X86
26
+ elif "arm" in arch:
27
+ return URL_MACOS_ARM
28
+ else:
29
+ raise Exception(f"Unsupported arch: {arch}")
30
+ else:
31
+ raise Exception("Unsupported system")
32
+
33
+
34
+ def _remove_signed_binary_requirements(out: Path) -> None:
35
+ if platform.system() == "Windows":
36
+ return
37
+ # mac os
38
+ if platform.system() == "Darwin":
39
+ # remove signed binary requirements
40
+ #
41
+ # xattr -d com.apple.quarantine rclone
42
+ import subprocess
43
+
44
+ subprocess.run(
45
+ ["xattr", "-d", "com.apple.quarantine", str(out)],
46
+ capture_output=True,
47
+ check=False,
48
+ )
49
+ return
50
+
51
+
52
+ def _make_executable(out: Path) -> None:
53
+ if platform.system() == "Windows":
54
+ return
55
+ # linux and mac os
56
+ os.chmod(out, 0o755)
57
+
58
+
59
+ def _find_rclone_exe(start: Path) -> Path | None:
60
+ for root, dirs, files in os.walk(start):
61
+ if platform.system() == "Windows":
62
+ if "rclone.exe" in files:
63
+ return Path(root) / "rclone.exe"
64
+ else:
65
+ if "rclone" in files:
66
+ return Path(root) / "rclone"
67
+ return None
68
+
69
+
70
+ def rclone_download(out: Path, replace=False) -> Exception | None:
71
+ try:
72
+ url = rclone_download_url()
73
+ with TemporaryDirectory() as tmpdir:
74
+ tmp = Path(tmpdir)
75
+ download(url, tmp, kind="zip", replace=replace)
76
+ exe = _find_rclone_exe(tmp)
77
+ if exe is None:
78
+ raise FileNotFoundError("rclone executable not found")
79
+ if os.path.exists(out):
80
+ os.remove(out)
81
+ shutil.move(exe, out)
82
+ _remove_signed_binary_requirements(out)
83
+ _make_executable(out)
84
+ return None
85
+ except Exception as e:
86
+ import traceback
87
+
88
+ stacktrace = traceback.format_exc()
89
+ warn(f"Failed to download rclone: {e}\n{stacktrace}")
90
+ return e
rclone_api/log.py CHANGED
@@ -1,44 +1,44 @@
1
- import logging
2
- import sys
3
-
4
- _INITIALISED = False
5
-
6
-
7
- def setup_default_logging():
8
- """Set up default logging configuration if none exists."""
9
- global _INITIALISED
10
- if _INITIALISED:
11
- return
12
- if not logging.root.handlers:
13
- logging.basicConfig(
14
- level=logging.INFO,
15
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
16
- handlers=[
17
- logging.StreamHandler(sys.stdout),
18
- # Uncomment to add file logging
19
- # logging.FileHandler('rclone_api.log')
20
- ],
21
- )
22
-
23
-
24
- def configure_logging(level=logging.INFO, log_file=None):
25
- """Configure logging for the rclone_api package.
26
-
27
- Args:
28
- level: The logging level (default: logging.INFO)
29
- log_file: Optional path to a log file
30
- """
31
- handlers = [logging.StreamHandler(sys.stdout)]
32
- if log_file:
33
- handlers.append(logging.FileHandler(log_file))
34
-
35
- logging.basicConfig(
36
- level=level,
37
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
- handlers=handlers,
39
- force=True, # Override any existing configuration
40
- )
41
-
42
-
43
- # Call setup_default_logging when this module is imported
44
- setup_default_logging()
1
+ import logging
2
+ import sys
3
+
4
+ _INITIALISED = False
5
+
6
+
7
+ def setup_default_logging():
8
+ """Set up default logging configuration if none exists."""
9
+ global _INITIALISED
10
+ if _INITIALISED:
11
+ return
12
+ if not logging.root.handlers:
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
16
+ handlers=[
17
+ logging.StreamHandler(sys.stdout),
18
+ # Uncomment to add file logging
19
+ # logging.FileHandler('rclone_api.log')
20
+ ],
21
+ )
22
+
23
+
24
+ def configure_logging(level=logging.INFO, log_file=None):
25
+ """Configure logging for the rclone_api package.
26
+
27
+ Args:
28
+ level: The logging level (default: logging.INFO)
29
+ log_file: Optional path to a log file
30
+ """
31
+ handlers = [logging.StreamHandler(sys.stdout)]
32
+ if log_file:
33
+ handlers.append(logging.FileHandler(log_file))
34
+
35
+ logging.basicConfig(
36
+ level=level,
37
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
+ handlers=handlers,
39
+ force=True, # Override any existing configuration
40
+ )
41
+
42
+
43
+ # Call setup_default_logging when this module is imported
44
+ setup_default_logging()