rclone-api 1.5.49__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
@@ -24,7 +24,7 @@ from .dir_listing import DirListing # Directory contents representation
24
24
  from .file import File, FileItem # File representation
25
25
  from .file_stream import FilesStream # Streaming file listings
26
26
  from .filelist import FileList # File list utilities
27
- from .fs import FSPath, RealFS, RemoteFS # Filesystem utilities
27
+ from .fs.filesystem import FSPath, RealFS, RemoteFS # Filesystem utilities
28
28
  from .http_server import HttpFetcher, HttpServer, Range # HTTP serving capabilities
29
29
 
30
30
  # Import logging configuration utilities
@@ -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
@@ -26,13 +26,14 @@ from rclone_api.dir_listing import DirListing
26
26
  from rclone_api.exec import RcloneExec
27
27
  from rclone_api.file import File
28
28
  from rclone_api.file_stream import FilesStream
29
- from rclone_api.fs import FSPath, RemoteFS
29
+ from rclone_api.fs.filesystem import FSPath, RemoteFS
30
30
  from rclone_api.group_files import group_files
31
31
  from rclone_api.http_server import HttpServer
32
32
  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.49
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=KxOU2UGnoqw1EDHwugFTHHgccfhq22trGiRDwX5Dsng,34086
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
@@ -13,7 +13,6 @@ rclone_api/file_item.py,sha256=cH-AQYsxedhNPp4c8NHY1ad4Z7St4yf_VGbmiGD59no,1770
13
13
  rclone_api/file_part.py,sha256=i6ByS5_sae8Eba-4imBVTxd-xKC8ExWy7NR8QGr0ors,6155
14
14
  rclone_api/file_stream.py,sha256=_W3qnwCuigqA0hzXl2q5pAxSZDRaUSwet4BkT0lpnzs,1431
15
15
  rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
16
- rclone_api/fs.py,sha256=xUkoSV2R9dByKgueZ3x2GjyCn9fM7tK1ydgK1Whiph0,8933
17
16
  rclone_api/group_files.py,sha256=H92xPW9lQnbNw5KbtZCl00bD6iRh9yRbCuxku4j_3dg,8036
18
17
  rclone_api/http_server.py,sha256=P-LT2GqCEM9tGbyzzr-CrSOc3FyCw1qXUSQ5bY2KBHU,12113
19
18
  rclone_api/install.py,sha256=ZDG8QNj1JciS_DSqYnMTECwhJksUPAoqZQxtX804TDk,5679
@@ -21,11 +20,11 @@ rclone_api/log.py,sha256=VZHM7pNSXip2ZLBKMP7M1u-rp_F7zoafFDuR8CPUoKI,1271
21
20
  rclone_api/mount.py,sha256=LZqEhuKZunbWVqmsOIqkkCotaxWJpdFRS1InXveoU5E,1428
22
21
  rclone_api/mount_util.py,sha256=jqhJEVTHV6c6lOOzUYb4FLMbqDMHdz7-QRcdH-IobFc,10154
23
22
  rclone_api/process.py,sha256=MeWiN-TrrN0HmtWexBPxGwf84z6f-_E5yaXE-YtLYpY,5879
24
- rclone_api/rclone_impl.py,sha256=EFaHco5ZkZV-BsTP49QUvgyPVoDCX_FuWW6OVS4ehTI,50139
23
+ rclone_api/rclone_impl.py,sha256=AXj74tSOsR3bnVtAYNcHOIHH74krRXvPggWaMdnEwm4,52126
25
24
  rclone_api/remote.py,sha256=mTgMTQTwxUmbLjTpr-AGTId2ycXKI9mLX5L7PPpDIoc,520
26
25
  rclone_api/rpath.py,sha256=Y1JjQWcie39EgQrq-UtbfDz5yDLCwwfu27W7AQXllSE,2860
27
26
  rclone_api/scan_missing_folders.py,sha256=-8NCwpCaHeHrX-IepCoAEsX1rl8S-GOCxcIhTr_w3gA,4747
28
- rclone_api/types.py,sha256=2ngxwpdNy88y0teeYJ5Vz5NiLK1rfaFx8Xf99i0J-Js,12155
27
+ rclone_api/types.py,sha256=59Rw7NdHw35X6iiWMemO61IzCbrjgfNHUlzY3Dq0wdM,12303
29
28
  rclone_api/util.py,sha256=Xa_VifjNT_h9IMkqbnAmgSkEqqwczkGH_TcUUJHx-no,9546
30
29
  rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
31
30
  rclone_api/cmd/analyze.py,sha256=RHbvk1G5ZUc3qLqlm1AZEyQzd_W_ZjcbCNDvW4YpTKQ,1252
@@ -41,7 +40,8 @@ rclone_api/detail/copy_file_parts_resumable.py,sha256=RoUWV2eBWEvuuTfsvrz5BhtvX3
41
40
  rclone_api/detail/walk.py,sha256=-54NVE8EJcCstwDoaC_UtHm73R2HrZwVwQmsnv55xNU,3369
42
41
  rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
43
42
  rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
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.49.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
59
- rclone_api-1.5.49.dist-info/METADATA,sha256=7aSdgy1mRWaAttLY3wdMnqiQMoEvS-zzfnZplOvEaqY,37305
60
- rclone_api-1.5.49.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
61
- rclone_api-1.5.49.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
62
- rclone_api-1.5.49.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
63
- rclone_api-1.5.49.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,,