rclone-api 1.5.50__py3-none-any.whl → 1.5.52__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
@@ -2,6 +2,7 @@
2
2
  Unit test file.
3
3
  """
4
4
 
5
+ import logging
5
6
  import os
6
7
  import random
7
8
  import subprocess
@@ -33,6 +34,7 @@ from rclone_api.mount import Mount
33
34
  from rclone_api.process import Process
34
35
  from rclone_api.remote import Remote
35
36
  from rclone_api.rpath import RPath
37
+ from rclone_api.s3.api import S3Client
36
38
  from rclone_api.s3.create import S3Credentials
37
39
  from rclone_api.s3.types import (
38
40
  S3Provider,
@@ -56,6 +58,8 @@ from rclone_api.util import (
56
58
  # Enable tracing memory usage always
57
59
  tracemalloc.start()
58
60
 
61
+ logger = logging.getLogger(__name__)
62
+
59
63
 
60
64
  def rclone_verbose(verbose: bool | None) -> bool:
61
65
  if verbose is not None:
@@ -825,6 +829,52 @@ class RcloneImpl:
825
829
  except subprocess.CalledProcessError:
826
830
  return False
827
831
 
832
+ def _s3_client(self, src: str, verbose: bool | None = None) -> S3Client:
833
+ """Get an S3 client."""
834
+ verbose = get_verbose(verbose)
835
+ s3_creds = self.get_s3_credentials(remote=src, verbose=verbose)
836
+ s3_client = S3Client(s3_creds=s3_creds, verbose=verbose)
837
+ return s3_client
838
+
839
+ def copy_file_s3(
840
+ self,
841
+ src: Path,
842
+ dst: str,
843
+ verbose: bool | None = None,
844
+ ) -> Exception | None:
845
+ """Copy a file to S3."""
846
+ from rclone_api.s3.types import S3UploadTarget
847
+ from rclone_api.util import S3PathInfo
848
+
849
+ dst_is_s3 = self.is_s3(dst)
850
+ if not dst_is_s3:
851
+ return ValueError(f"Destination is not an S3 remote: {dst}")
852
+ s3_client = self._s3_client(dst, verbose=verbose)
853
+
854
+ path_info: S3PathInfo = S3PathInfo.from_str(dst)
855
+ target: S3UploadTarget = S3UploadTarget(
856
+ src_file=src,
857
+ src_file_size=src.stat().st_size,
858
+ bucket_name=path_info.bucket,
859
+ s3_key=path_info.key,
860
+ )
861
+ out = s3_client.upload_file(target=target)
862
+ return out
863
+
864
+ def is_s3(self, dst: str) -> bool:
865
+ """Check if a remote is an S3 remote."""
866
+ from rclone_api.util import S3PathInfo
867
+
868
+ path_info: S3PathInfo = S3PathInfo.from_str(dst)
869
+ remote = path_info.remote
870
+ parsed: Parsed = self.config.parse()
871
+ sections: dict[str, Section] = parsed.sections
872
+ if remote not in sections:
873
+ return False
874
+ section: Section = sections[remote]
875
+ t = section.type()
876
+ return t in ["s3", "b2"]
877
+
828
878
  def copy_file_s3_resumable(
829
879
  self,
830
880
  src: str, # src:/Bucket/path/myfile.large.zst
@@ -864,15 +914,28 @@ class RcloneImpl:
864
914
  def write_bytes(
865
915
  self,
866
916
  dst: str,
867
- data: bytes,
917
+ data: bytes | Path,
918
+ verbose: bool | None = None,
868
919
  ) -> Exception | None:
869
920
  """Write bytes to a file."""
870
- with TemporaryDirectory() as tmpdir:
871
- tmpfile = Path(tmpdir) / "file.bin"
872
- tmpfile.write_bytes(data)
873
- completed_proc = self.copy_to(str(tmpfile), dst, check=True)
874
- if completed_proc.returncode != 0:
875
- return Exception(f"Failed to write bytes to {dst}", completed_proc)
921
+
922
+ try:
923
+ if isinstance(data, Path):
924
+ data = data.read_bytes()
925
+
926
+ with TemporaryDirectory() as tmpdir:
927
+ tmpfile = Path(tmpdir) / "file.bin"
928
+ tmpfile.write_bytes(data)
929
+ dst_is_s3 = self.is_s3(dst)
930
+ if dst_is_s3:
931
+ return self.copy_file_s3(tmpfile, dst, verbose=verbose)
932
+
933
+ completed_proc = self.copy_to(str(tmpfile), dst, check=True)
934
+ if completed_proc.returncode != 0:
935
+ return Exception(f"Failed to write bytes to {dst}", completed_proc)
936
+ except Exception as e:
937
+ logging.error(f"Failed to write bytes to {dst}")
938
+ return e
876
939
  return None
877
940
 
878
941
  def read_bytes(self, src: str) -> bytes | Exception:
@@ -925,10 +988,10 @@ class RcloneImpl:
925
988
  def get_s3_credentials(
926
989
  self, remote: str, verbose: bool | None = None
927
990
  ) -> S3Credentials:
928
- from rclone_api.util import S3PathInfo, split_s3_path
991
+ from rclone_api.util import S3PathInfo
929
992
 
930
993
  verbose = get_verbose(verbose)
931
- path_info: S3PathInfo = split_s3_path(remote)
994
+ path_info: S3PathInfo = S3PathInfo.from_str(remote)
932
995
 
933
996
  # path_info: S3PathInfo = split_s3_path(remote)
934
997
  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.52
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=s00SHQGcKskbauWiIZQBQKSR0NOJ6SSiXhMf0fwkl9U,52328
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.52.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
59
+ rclone_api-1.5.52.dist-info/METADATA,sha256=pzUkl7zLuD22M8z-gOpbEkACDpT0tZ6r62N3hJ-LwMc,37305
60
+ rclone_api-1.5.52.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
61
+ rclone_api-1.5.52.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
62
+ rclone_api-1.5.52.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
63
+ rclone_api-1.5.52.dist-info/RECORD,,