rclone-api 1.5.37__py3-none-any.whl → 1.5.39__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/config.py CHANGED
@@ -1,153 +1,186 @@
1
- import os
2
- from dataclasses import dataclass, field
3
- from pathlib import Path
4
- from typing import Any, Dict, List
5
-
6
-
7
- @dataclass
8
- class Section:
9
- name: str
10
- data: Dict[str, str] = field(default_factory=dict)
11
-
12
- def add(self, key: str, value: str) -> None:
13
- self.data[key] = value
14
-
15
- def type(self) -> str:
16
- return self.data["type"]
17
-
18
- def provider(self) -> str | None:
19
- return self.data.get("provider")
20
-
21
- def access_key_id(self) -> str:
22
- if "access_key_id" in self.data:
23
- return self.data["access_key_id"]
24
- elif "account" in self.data:
25
- return self.data["account"]
26
- raise KeyError("No access key found")
27
-
28
- def secret_access_key(self) -> str:
29
- # return self.data["secret_access_key"]
30
- if "secret_access_key" in self.data:
31
- return self.data["secret_access_key"]
32
- elif "key" in self.data:
33
- return self.data["key"]
34
- raise KeyError("No secret access key found")
35
-
36
- def endpoint(self) -> str | None:
37
- return self.data.get("endpoint")
38
-
39
-
40
- @dataclass
41
- class Parsed:
42
- # sections: List[ParsedSection]
43
- sections: dict[str, Section]
44
-
45
- @staticmethod
46
- def parse(content: str) -> "Parsed":
47
- return parse_rclone_config(content)
48
-
49
-
50
- @dataclass
51
- class Config:
52
- """Rclone configuration dataclass."""
53
-
54
- text: str
55
-
56
- def parse(self) -> Parsed:
57
- return Parsed.parse(self.text)
58
-
59
-
60
- def find_conf_file(rclone: Any | None = None) -> Path | None:
61
- import subprocess
62
-
63
- from rclone_api import Rclone
64
- from rclone_api.rclone_impl import RcloneImpl
65
-
66
- # if os.environ.get("RCLONE_CONFIG"):
67
- # return Path(os.environ["RCLONE_CONFIG"])
68
- # return None
69
- # rclone_conf = rclone_conf or Path.cwd() / "rclone.conf"
70
-
71
- if os.environ.get("RCLONE_CONFIG"):
72
- return Path(os.environ["RCLONE_CONFIG"])
73
- if (conf := Path.cwd() / "rclone.conf").exists():
74
- return conf
75
-
76
- if rclone is None:
77
- from rclone_api.install import rclone_download
78
-
79
- err = rclone_download(Path("."))
80
- if isinstance(err, Exception):
81
- import warnings
82
-
83
- warnings.warn(f"rclone_download failed: {err}")
84
- return None
85
- cmd_list: list[str] = [
86
- "rclone",
87
- "config",
88
- "paths",
89
- ]
90
- subproc: subprocess.CompletedProcess = subprocess.run(
91
- args=cmd_list,
92
- shell=True,
93
- capture_output=True,
94
- text=True,
95
- )
96
- if subproc.returncode == 0:
97
- stdout = subproc.stdout
98
- for line in stdout.splitlines():
99
- parts = line.split(":", 1)
100
- if len(parts) == 2:
101
- _, value = parts
102
- value = value.strip()
103
- value_path = Path(value.strip())
104
- if value_path.exists():
105
- return value_path
106
- else:
107
- if isinstance(rclone, Rclone):
108
- rclone = rclone.impl
109
- else:
110
- assert isinstance(rclone, RcloneImpl)
111
- rclone_impl: RcloneImpl = rclone
112
- assert isinstance(rclone_impl, RcloneImpl)
113
- paths_or_err = rclone_impl.config_paths()
114
- if isinstance(paths_or_err, Exception):
115
- return None
116
- paths = paths_or_err
117
- path: Path
118
- for path in paths:
119
- if path.exists():
120
- return path
121
- return None
122
-
123
-
124
- def parse_rclone_config(content: str) -> Parsed:
125
- """
126
- Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
127
-
128
- Each section in the file starts with a line like [section_name]
129
- followed by key=value pairs.
130
- """
131
- sections: List[Section] = []
132
- current_section: Section | None = None
133
-
134
- lines = content.splitlines()
135
- for line in lines:
136
- line = line.strip()
137
- # Skip empty lines and comments (assumed to start with '#' or ';')
138
- if not line or line.startswith(("#", ";")):
139
- continue
140
- # New section header detected
141
- if line.startswith("[") and line.endswith("]"):
142
- section_name = line[1:-1].strip()
143
- current_section = Section(name=section_name)
144
- sections.append(current_section)
145
- elif "=" in line and current_section is not None:
146
- # Parse key and value, splitting only on the first '=' found
147
- key, value = line.split("=", 1)
148
- current_section.add(key.strip(), value.strip())
149
-
150
- data: dict[str, Section] = {}
151
- for section in sections:
152
- data[section.name] = section
153
- return Parsed(sections=data)
1
+ import os
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List
5
+
6
+
7
+ @dataclass
8
+ class Section:
9
+ name: str
10
+ data: Dict[str, str] = field(default_factory=dict)
11
+
12
+ def add(self, key: str, value: str) -> None:
13
+ self.data[key] = value
14
+
15
+ def type(self) -> str:
16
+ return self.data["type"]
17
+
18
+ def provider(self) -> str | None:
19
+ return self.data.get("provider")
20
+
21
+ def access_key_id(self) -> str:
22
+ if "access_key_id" in self.data:
23
+ return self.data["access_key_id"]
24
+ elif "account" in self.data:
25
+ return self.data["account"]
26
+ raise KeyError("No access key found")
27
+
28
+ def secret_access_key(self) -> str:
29
+ # return self.data["secret_access_key"]
30
+ if "secret_access_key" in self.data:
31
+ return self.data["secret_access_key"]
32
+ elif "key" in self.data:
33
+ return self.data["key"]
34
+ raise KeyError("No secret access key found")
35
+
36
+ def endpoint(self) -> str | None:
37
+ return self.data.get("endpoint")
38
+
39
+
40
+ @dataclass
41
+ class Parsed:
42
+ # sections: List[ParsedSection]
43
+ sections: dict[str, Section]
44
+
45
+ @staticmethod
46
+ def parse(content: str) -> "Parsed":
47
+ return parse_rclone_config(content)
48
+
49
+
50
+ @dataclass
51
+ class Config:
52
+ """Rclone configuration dataclass."""
53
+
54
+ text: str
55
+
56
+ @staticmethod
57
+ def from_json(json_data: dict) -> "Config | Exception":
58
+ return json_to_rclone_config(json_data)
59
+
60
+ def parse(self) -> Parsed:
61
+ return Parsed.parse(self.text)
62
+
63
+
64
+ def find_conf_file(rclone: Any | None = None) -> Path | None:
65
+ import subprocess
66
+
67
+ from rclone_api import Rclone
68
+ from rclone_api.rclone_impl import RcloneImpl
69
+
70
+ # if os.environ.get("RCLONE_CONFIG"):
71
+ # return Path(os.environ["RCLONE_CONFIG"])
72
+ # return None
73
+ # rclone_conf = rclone_conf or Path.cwd() / "rclone.conf"
74
+
75
+ if os.environ.get("RCLONE_CONFIG"):
76
+ return Path(os.environ["RCLONE_CONFIG"])
77
+ if (conf := Path.cwd() / "rclone.conf").exists():
78
+ return conf
79
+
80
+ if rclone is None:
81
+ from rclone_api.install import rclone_download
82
+
83
+ err = rclone_download(Path("."))
84
+ if isinstance(err, Exception):
85
+ import warnings
86
+
87
+ warnings.warn(f"rclone_download failed: {err}")
88
+ return None
89
+ cmd_list: list[str] = [
90
+ "rclone",
91
+ "config",
92
+ "paths",
93
+ ]
94
+ subproc: subprocess.CompletedProcess = subprocess.run(
95
+ args=cmd_list,
96
+ shell=True,
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ if subproc.returncode == 0:
101
+ stdout = subproc.stdout
102
+ for line in stdout.splitlines():
103
+ parts = line.split(":", 1)
104
+ if len(parts) == 2:
105
+ _, value = parts
106
+ value = value.strip()
107
+ value_path = Path(value.strip())
108
+ if value_path.exists():
109
+ return value_path
110
+ else:
111
+ if isinstance(rclone, Rclone):
112
+ rclone = rclone.impl
113
+ else:
114
+ assert isinstance(rclone, RcloneImpl)
115
+ rclone_impl: RcloneImpl = rclone
116
+ assert isinstance(rclone_impl, RcloneImpl)
117
+ paths_or_err = rclone_impl.config_paths()
118
+ if isinstance(paths_or_err, Exception):
119
+ return None
120
+ paths = paths_or_err
121
+ path: Path
122
+ for path in paths:
123
+ if path.exists():
124
+ return path
125
+ return None
126
+
127
+
128
+ def parse_rclone_config(content: str) -> Parsed:
129
+ """
130
+ Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
131
+
132
+ Each section in the file starts with a line like [section_name]
133
+ followed by key=value pairs.
134
+ """
135
+ sections: List[Section] = []
136
+ current_section: Section | None = None
137
+
138
+ lines = content.splitlines()
139
+ for line in lines:
140
+ line = line.strip()
141
+ # Skip empty lines and comments (assumed to start with '#' or ';')
142
+ if not line or line.startswith(("#", ";")):
143
+ continue
144
+ # New section header detected
145
+ if line.startswith("[") and line.endswith("]"):
146
+ section_name = line[1:-1].strip()
147
+ current_section = Section(name=section_name)
148
+ sections.append(current_section)
149
+ elif "=" in line and current_section is not None:
150
+ # Parse key and value, splitting only on the first '=' found
151
+ key, value = line.split("=", 1)
152
+ current_section.add(key.strip(), value.strip())
153
+
154
+ data: dict[str, Section] = {}
155
+ for section in sections:
156
+ data[section.name] = section
157
+ return Parsed(sections=data)
158
+
159
+
160
+ # JSON_DATA = {
161
+ # "dst": {
162
+ # "type": "s3",
163
+ # "bucket": "bucket",
164
+ # "endpoint": "https://s3.amazonaws.com",
165
+ # "access_key_id": "access key",
166
+ # "access_secret_key": "access secret key",
167
+ # }
168
+ # }
169
+
170
+
171
+ def _json_to_rclone_config_str_or_raise(json_data: dict) -> str:
172
+ """Convert JSON data to rclone config."""
173
+ out = ""
174
+ for key, value in json_data.items():
175
+ out += f"[{key}]\n"
176
+ for k, v in value.items():
177
+ out += f"{k} = {v}\n"
178
+ return out
179
+
180
+
181
+ def json_to_rclone_config(json_data: dict) -> Config | Exception:
182
+ try:
183
+ text = _json_to_rclone_config_str_or_raise(json_data)
184
+ return Config(text=text)
185
+ except Exception as e:
186
+ return e
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