rclone-api 1.5.50__py3-none-any.whl → 1.5.51__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 CHANGED
@@ -438,6 +438,37 @@ class Rclone:
438
438
  """
439
439
  return self.impl.cleanup(src=src, other_args=other_args)
440
440
 
441
+ def copy_file_s3(
442
+ self,
443
+ src: Path,
444
+ dst: str,
445
+ verbose: bool | None = None,
446
+ ) -> Exception | None:
447
+ """
448
+ Copy a file to S3.
449
+
450
+ Args:
451
+ src: Local file path
452
+ dst: Destination path in S3
453
+ verbose: Whether to show detailed progress
454
+
455
+ Returns:
456
+ None if successful, Exception if an error occurred
457
+ """
458
+ return self.impl.copy_file_s3(src=src, dst=dst, verbose=verbose)
459
+
460
+ def is_s3(self, dst: str) -> bool:
461
+ """
462
+ Check if a path is an S3 bucket.
463
+
464
+ Args:
465
+ dst: Path to check
466
+
467
+ Returns:
468
+ True if the path is an S3 bucket, False otherwise
469
+ """
470
+ return self.impl.is_s3(dst=dst)
471
+
441
472
  def copy_to(
442
473
  self,
443
474
  src: File | str,
@@ -1,17 +1,20 @@
1
1
  import abc
2
+ import logging
2
3
  import shutil
3
4
  import warnings
4
5
  from pathlib import Path
5
6
 
6
7
  from rclone_api.config import Config
7
8
 
9
+ logger = logging.getLogger(__name__)
10
+
8
11
 
9
12
  class FS(abc.ABC):
10
13
  def __init__(self) -> None:
11
14
  pass
12
15
 
13
16
  @abc.abstractmethod
14
- def copy(self, src: Path | str, dest: Path | str) -> None:
17
+ def copy(self, src: Path | str, dst: Path | str) -> None:
15
18
  pass
16
19
 
17
20
  @abc.abstractmethod
@@ -67,8 +70,8 @@ class RealFS(FS):
67
70
  def cwd(self) -> "FSPath":
68
71
  return RealFS.from_path(Path.cwd())
69
72
 
70
- def copy(self, src: Path | str, dest: Path | str) -> None:
71
- shutil.copy(str(src), str(dest))
73
+ def copy(self, src: Path | str, dst: Path | str) -> None:
74
+ shutil.copy(str(src), str(dst))
72
75
 
73
76
  def read_bytes(self, path: Path | str) -> bytes:
74
77
  with open(path, "rb") as f:
@@ -132,10 +135,31 @@ class RemoteFS(FS):
132
135
  def _to_remote_path(self, path: str | Path) -> str:
133
136
  return Path(path).relative_to(self.src).as_posix()
134
137
 
135
- def copy(self, src: Path | str, dest: Path | str) -> None:
136
- src = self._to_str(src)
137
- dest = self._to_remote_path(dest)
138
- self.rclone.copy(src, dest)
138
+ def copy(self, src: Path | str, dst: Path | str) -> None:
139
+ from rclone_api.completed_process import CompletedProcess
140
+
141
+ src = src if isinstance(src, Path) else Path(src)
142
+ if not src.is_file():
143
+ raise FileNotFoundError(f"File not found: {src}")
144
+ dst = self._to_remote_path(dst)
145
+
146
+ is_s3 = self.rclone.is_s3(dst)
147
+ if is_s3:
148
+ filesize = src.stat().st_size
149
+ if filesize < 512 * 1024 * 1024:
150
+ logger.info(f"S3 OPTIMIZED: Copying {src} -> {dst}")
151
+ err = self.rclone.copy_file_s3(src, dst)
152
+ if isinstance(err, Exception):
153
+ raise FileNotFoundError(
154
+ f"File not found: {src}, specified by {err}"
155
+ )
156
+ return
157
+ # Fallback.
158
+ logging.info(f"Copying {src} -> {dst}")
159
+ src_path = src.as_posix()
160
+ cp: CompletedProcess = self.rclone.copy(src_path, dst)
161
+ if cp.returncode != 0:
162
+ raise FileNotFoundError(f"File not found: {src}, specified by {cp.stderr}")
139
163
 
140
164
  def read_bytes(self, path: Path | str) -> bytes:
141
165
  path = self._to_str(path)
@@ -146,7 +170,7 @@ class RemoteFS(FS):
146
170
 
147
171
  def write_binary(self, path: Path | str, data: bytes) -> None:
148
172
  path = self._to_str(path)
149
- self.rclone.write_bytes(data, path)
173
+ self.rclone.write_bytes(data, path) # Already optimized for s3.
150
174
 
151
175
  def exists(self, path: Path | str) -> bool:
152
176
  from rclone_api.http_server import HttpServer
rclone_api/rclone_impl.py CHANGED
@@ -33,6 +33,7 @@ from rclone_api.mount import Mount
33
33
  from rclone_api.process import Process
34
34
  from rclone_api.remote import Remote
35
35
  from rclone_api.rpath import RPath
36
+ from rclone_api.s3.api import S3Client
36
37
  from rclone_api.s3.create import S3Credentials
37
38
  from rclone_api.s3.types import (
38
39
  S3Provider,
@@ -825,6 +826,51 @@ class RcloneImpl:
825
826
  except subprocess.CalledProcessError:
826
827
  return False
827
828
 
829
+ def _s3_client(self, src: str, verbose: bool | None = None) -> S3Client:
830
+ """Get an S3 client."""
831
+ verbose = get_verbose(verbose)
832
+ s3_creds = self.get_s3_credentials(remote=src, verbose=verbose)
833
+ s3_client = S3Client(s3_creds=s3_creds, verbose=verbose)
834
+ return s3_client
835
+
836
+ def copy_file_s3(
837
+ self,
838
+ src: Path,
839
+ dst: str,
840
+ verbose: bool | None = None,
841
+ ) -> Exception | None:
842
+ """Copy a file to S3."""
843
+ from rclone_api.s3.types import S3UploadTarget
844
+ from rclone_api.util import S3PathInfo
845
+
846
+ dst_is_s3 = self.is_s3(dst)
847
+ if not dst_is_s3:
848
+ return ValueError(f"Destination is not an S3 remote: {dst}")
849
+ s3_client = self._s3_client(dst, verbose=verbose)
850
+
851
+ path_info: S3PathInfo = S3PathInfo.from_str(dst)
852
+ target: S3UploadTarget = S3UploadTarget(
853
+ src_file=src,
854
+ src_file_size=src.stat().st_size,
855
+ bucket_name=path_info.bucket,
856
+ s3_key=path_info.key,
857
+ )
858
+ out = s3_client.upload_file(target=target)
859
+ return out
860
+
861
+ def is_s3(self, dst: str) -> bool:
862
+ """Check if a remote is an S3 remote."""
863
+ from rclone_api.util import S3PathInfo
864
+
865
+ path_info: S3PathInfo = S3PathInfo.from_str(dst)
866
+ remote = path_info.remote
867
+ parsed: Parsed = self.config.parse()
868
+ sections: dict[str, Section] = parsed.sections
869
+ if remote not in sections:
870
+ raise ValueError(f"Remote {remote} not found in rclone config")
871
+ section: Section = sections[remote]
872
+ return section.type() == "s3"
873
+
828
874
  def copy_file_s3_resumable(
829
875
  self,
830
876
  src: str, # src:/Bucket/path/myfile.large.zst
@@ -864,12 +910,21 @@ class RcloneImpl:
864
910
  def write_bytes(
865
911
  self,
866
912
  dst: str,
867
- data: bytes,
913
+ data: bytes | Path,
914
+ verbose: bool | None = None,
868
915
  ) -> Exception | None:
869
916
  """Write bytes to a file."""
917
+
918
+ if isinstance(data, Path):
919
+ data = data.read_bytes()
920
+
870
921
  with TemporaryDirectory() as tmpdir:
871
922
  tmpfile = Path(tmpdir) / "file.bin"
872
923
  tmpfile.write_bytes(data)
924
+ dst_is_s3 = self.is_s3(dst)
925
+ if dst_is_s3:
926
+ return self.copy_file_s3(tmpfile, dst, verbose=verbose)
927
+
873
928
  completed_proc = self.copy_to(str(tmpfile), dst, check=True)
874
929
  if completed_proc.returncode != 0:
875
930
  return Exception(f"Failed to write bytes to {dst}", completed_proc)
@@ -925,10 +980,10 @@ class RcloneImpl:
925
980
  def get_s3_credentials(
926
981
  self, remote: str, verbose: bool | None = None
927
982
  ) -> S3Credentials:
928
- from rclone_api.util import S3PathInfo, split_s3_path
983
+ from rclone_api.util import S3PathInfo
929
984
 
930
985
  verbose = get_verbose(verbose)
931
- path_info: S3PathInfo = split_s3_path(remote)
986
+ path_info: S3PathInfo = S3PathInfo.from_str(remote)
932
987
 
933
988
  # path_info: S3PathInfo = split_s3_path(remote)
934
989
  remote = path_info.remote
rclone_api/s3/api.py CHANGED
@@ -27,6 +27,10 @@ class S3Client:
27
27
  s3_creds=s3_creds, s3_config=S3Config(verbose=verbose)
28
28
  )
29
29
 
30
+ @property
31
+ def bucket_name(self) -> str:
32
+ return self.credentials.bucket_name
33
+
30
34
  def list_bucket_contents(self, bucket_name: str) -> None:
31
35
  list_bucket_contents(self.client, bucket_name)
32
36
 
rclone_api/types.py CHANGED
@@ -31,6 +31,12 @@ class S3PathInfo:
31
31
  bucket: str
32
32
  key: str
33
33
 
34
+ @staticmethod
35
+ def from_str(src: str) -> "S3PathInfo":
36
+ from rclone_api.util import split_s3_path
37
+
38
+ return split_s3_path(src)
39
+
34
40
 
35
41
  @dataclass
36
42
  class SizeResult:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rclone_api
3
- Version: 1.5.50
3
+ Version: 1.5.51
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -1,4 +1,4 @@
1
- rclone_api/__init__.py,sha256=SDvu6dsXpgJTnsqT6aH8P-46lWTirF99tgXToj-_R94,34097
1
+ rclone_api/__init__.py,sha256=M3-jlyxhO7Md3FGMO8qBJ1Z7oLcbOjWshCHh2-CqKG4,34885
2
2
  rclone_api/cli.py,sha256=dibfAZIh0kXWsBbfp3onKLjyZXo54mTzDjUdzJlDlWo,231
3
3
  rclone_api/completed_process.py,sha256=_IZ8IWK7DM1_tsbDEkH6wPZ-bbcrgf7A7smls854pmg,1775
4
4
  rclone_api/config.py,sha256=DEBubN0ziUfAFYe6cmDn7YoTqRJPdGronFyRHZvWUso,6070
@@ -20,11 +20,11 @@ rclone_api/log.py,sha256=VZHM7pNSXip2ZLBKMP7M1u-rp_F7zoafFDuR8CPUoKI,1271
20
20
  rclone_api/mount.py,sha256=LZqEhuKZunbWVqmsOIqkkCotaxWJpdFRS1InXveoU5E,1428
21
21
  rclone_api/mount_util.py,sha256=jqhJEVTHV6c6lOOzUYb4FLMbqDMHdz7-QRcdH-IobFc,10154
22
22
  rclone_api/process.py,sha256=MeWiN-TrrN0HmtWexBPxGwf84z6f-_E5yaXE-YtLYpY,5879
23
- rclone_api/rclone_impl.py,sha256=b9vcRH9__A8cYfeXaGnaLJG7JzdlaGTzVAiUaHNSO6g,50150
23
+ rclone_api/rclone_impl.py,sha256=AXj74tSOsR3bnVtAYNcHOIHH74krRXvPggWaMdnEwm4,52126
24
24
  rclone_api/remote.py,sha256=mTgMTQTwxUmbLjTpr-AGTId2ycXKI9mLX5L7PPpDIoc,520
25
25
  rclone_api/rpath.py,sha256=Y1JjQWcie39EgQrq-UtbfDz5yDLCwwfu27W7AQXllSE,2860
26
26
  rclone_api/scan_missing_folders.py,sha256=-8NCwpCaHeHrX-IepCoAEsX1rl8S-GOCxcIhTr_w3gA,4747
27
- rclone_api/types.py,sha256=2ngxwpdNy88y0teeYJ5Vz5NiLK1rfaFx8Xf99i0J-Js,12155
27
+ rclone_api/types.py,sha256=59Rw7NdHw35X6iiWMemO61IzCbrjgfNHUlzY3Dq0wdM,12303
28
28
  rclone_api/util.py,sha256=Xa_VifjNT_h9IMkqbnAmgSkEqqwczkGH_TcUUJHx-no,9546
29
29
  rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
30
30
  rclone_api/cmd/analyze.py,sha256=RHbvk1G5ZUc3qLqlm1AZEyQzd_W_ZjcbCNDvW4YpTKQ,1252
@@ -40,8 +40,8 @@ rclone_api/detail/copy_file_parts_resumable.py,sha256=RoUWV2eBWEvuuTfsvrz5BhtvX3
40
40
  rclone_api/detail/walk.py,sha256=-54NVE8EJcCstwDoaC_UtHm73R2HrZwVwQmsnv55xNU,3369
41
41
  rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
42
42
  rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
43
- rclone_api/fs/filesystem.py,sha256=xUkoSV2R9dByKgueZ3x2GjyCn9fM7tK1ydgK1Whiph0,8933
44
- rclone_api/s3/api.py,sha256=6E4xEOxtpP6niiAFEpgB1-ckWJclNyRsJ3D11Qm4RwU,4069
43
+ rclone_api/fs/filesystem.py,sha256=KgaLqk6OCePcWWtPgh9NDf3dMJzYpHdhQspvzgQ0r1s,9919
44
+ rclone_api/s3/api.py,sha256=HpheMOGHcCc0CyNdv0zvB0S92g312urjRdssRrkdpb8,4162
45
45
  rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
46
46
  rclone_api/s3/chunk_task.py,sha256=waEYe-iYQ1_BR3NCS4BrzVrK9UANvH1EcbXx2I6Z_NM,6839
47
47
  rclone_api/s3/create.py,sha256=_Q-faQ4Zl8XKTB28gireRxVXWP-YNxoAK4bligxDtiI,3998
@@ -55,9 +55,9 @@ rclone_api/s3/multipart/upload_parts_inline.py,sha256=V7syKjFyVIe4U9Ahl5XgqVTzt9
55
55
  rclone_api/s3/multipart/upload_parts_resumable.py,sha256=6-nlMclS8jyVvMvFbQDcZOX9MY1WbCcKA_s9bwuYxnk,9793
56
56
  rclone_api/s3/multipart/upload_parts_server_side_merge.py,sha256=Fp2pdrs5dONQI9LkfNolgAGj1-Z2V1SsRd0r0sreuXI,18040
57
57
  rclone_api/s3/multipart/upload_state.py,sha256=f-Aq2NqtAaMUMhYitlICSNIxCKurWAl2gDEUVizLIqw,6019
58
- rclone_api-1.5.50.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
59
- rclone_api-1.5.50.dist-info/METADATA,sha256=SoxiYtc9A7AVTeWiTMqqsSakoS1LXaTKisjQ5hpwZ08,37305
60
- rclone_api-1.5.50.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
61
- rclone_api-1.5.50.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
62
- rclone_api-1.5.50.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
63
- rclone_api-1.5.50.dist-info/RECORD,,
58
+ rclone_api-1.5.51.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
59
+ rclone_api-1.5.51.dist-info/METADATA,sha256=EWCOVITezwvrFLaLlSQGsCGLBTRdBuRKtz2h4u8tZW8,37305
60
+ rclone_api-1.5.51.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
61
+ rclone_api-1.5.51.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
62
+ rclone_api-1.5.51.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
63
+ rclone_api-1.5.51.dist-info/RECORD,,