rclone-api 1.1.30__py2.py3-none-any.whl → 1.1.32__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/exec.py CHANGED
@@ -23,7 +23,9 @@ class RcloneExec:
23
23
  cmd, self.rclone_config, self.rclone_exe, check=check, capture=capture
24
24
  )
25
25
 
26
- def launch_process(self, cmd: list[str], capture: bool | None) -> Process:
26
+ def launch_process(
27
+ self, cmd: list[str], capture: bool | None, log: Path | None
28
+ ) -> Process:
27
29
  """Launch rclone process."""
28
30
 
29
31
  args: ProcessArgs = ProcessArgs(
@@ -32,6 +34,7 @@ class RcloneExec:
32
34
  rclone_exe=self.rclone_exe,
33
35
  cmd_list=cmd,
34
36
  capture_stdout=capture,
37
+ log=log,
35
38
  )
36
39
  process = Process(args)
37
40
  return process
rclone_api/mount.py CHANGED
@@ -105,8 +105,8 @@ def clean_mount(mount: Mount | Path, verbose: bool = False, wait=True) -> None:
105
105
  mount_path = mount.mount_path if isinstance(mount, Mount) else mount
106
106
  try:
107
107
  mount_exists = mount_path.exists()
108
- except OSError as e:
109
- warnings.warn(f"Error checking {mount_path}: {e}")
108
+ except OSError:
109
+ # warnings.warn(f"Error checking {mount_path}: {e}")
110
110
  mount_exists = True
111
111
 
112
112
  # Give the system a moment (if unmount is in progress, etc.)
@@ -139,8 +139,9 @@ def clean_mount(mount: Mount | Path, verbose: bool = False, wait=True) -> None:
139
139
  mount_path.rmdir()
140
140
  if verbose:
141
141
  print(f"Successfully removed mount directory {mount_path}")
142
- except Exception as e:
143
- warnings.warn(f"Failed to remove mount {mount_path}: {e}")
142
+ except Exception:
143
+ # warnings.warn(f"Failed to remove mount {mount_path}: {e}")
144
+ pass
144
145
  else:
145
146
  warnings.warn(f"Unsupported platform: {_SYSTEM}")
146
147
 
rclone_api/process.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import atexit
2
- import os
3
2
  import subprocess
4
3
  import threading
5
4
  import time
@@ -13,12 +12,6 @@ from rclone_api.config import Config
13
12
  from rclone_api.util import get_verbose
14
13
 
15
14
 
16
- def _get_verbose(verbose: bool | None) -> bool:
17
- if verbose is not None:
18
- return verbose
19
- return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
20
-
21
-
22
15
  @dataclass
23
16
  class ProcessArgs:
24
17
  cmd: list[str]
@@ -27,12 +20,14 @@ class ProcessArgs:
27
20
  cmd_list: list[str]
28
21
  verbose: bool | None = None
29
22
  capture_stdout: bool | None = None
23
+ log: Path | None = None
30
24
 
31
25
 
32
26
  class Process:
33
27
  def __init__(self, args: ProcessArgs) -> None:
34
28
  assert args.rclone_exe.exists()
35
29
  self.args = args
30
+ self.log = args.log
36
31
  self.tempdir: TemporaryDirectory | None = None
37
32
  verbose = get_verbose(args.verbose)
38
33
  if isinstance(args.rclone_conf, Config):
@@ -52,6 +47,9 @@ class Process:
52
47
  + ["--config", str(rclone_conf.resolve())]
53
48
  + args.cmd
54
49
  )
50
+ if self.args.log:
51
+ self.args.log.parent.mkdir(parents=True, exist_ok=True)
52
+ self.cmd += ["--log-file", str(self.args.log)]
55
53
  if verbose:
56
54
  cmd_str = subprocess.list2cmdline(self.cmd)
57
55
  print(f"Running: {cmd_str}")
@@ -0,0 +1,146 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import unittest
8
+
9
+ import psutil
10
+ from dotenv import load_dotenv
11
+
12
+ from rclone_api import Config, Rclone, SizeSuffix
13
+
14
+ load_dotenv()
15
+
16
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
17
+
18
+
19
+ def _generate_rclone_config() -> Config:
20
+
21
+ # assert that .env exists for this test
22
+ assert os.path.exists(
23
+ ".env"
24
+ ), "this test requires that the secret .env file exists with the credentials"
25
+
26
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
27
+
28
+ # Load additional environment variables
29
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
30
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
31
+ SRC_SFTP_HOST = os.getenv("SRC_SFTP_HOST")
32
+ SRC_SFTP_USER = os.getenv("SRC_SFTP_USER")
33
+ SRC_SFTP_PORT = os.getenv("SRC_SFTP_PORT")
34
+ SRC_SFTP_PASS = os.getenv("SRC_SFTP_PASS")
35
+ # BUCKET_URL = os.getenv("BUCKET_URL")
36
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
37
+
38
+ config_text = f"""
39
+ [dst]
40
+ type = s3
41
+ provider = DigitalOcean
42
+ access_key_id = {BUCKET_KEY_PUBLIC}
43
+ secret_access_key = {BUCKET_KEY_SECRET}
44
+ endpoint = {BUCKET_URL}
45
+ bucket = {BUCKET_NAME}
46
+
47
+ [src]
48
+ type = sftp
49
+ host = {SRC_SFTP_HOST}
50
+ user = {SRC_SFTP_USER}
51
+ port = {SRC_SFTP_PORT}
52
+ pass = {SRC_SFTP_PASS}
53
+
54
+ """
55
+ # _CONFIG_PATH.write_text(config_text, encoding="utf-8")
56
+ # print(f"Config file written to: {_CONFIG_PATH}")
57
+ return Config(config_text)
58
+
59
+
60
+ class RcloneProfileCopyBytes(unittest.TestCase):
61
+ """Test rclone functionality."""
62
+
63
+ def setUp(self) -> None:
64
+ """Check if all required environment variables are set before running tests."""
65
+ required_vars = [
66
+ "BUCKET_NAME",
67
+ "BUCKET_KEY_SECRET",
68
+ "BUCKET_KEY_PUBLIC",
69
+ "BUCKET_URL",
70
+ ]
71
+ missing = [var for var in required_vars if not os.getenv(var)]
72
+ if missing:
73
+ self.skipTest(
74
+ f"Missing required environment variables: {', '.join(missing)}"
75
+ )
76
+ os.environ["RCLONE_API_VERBOSE"] = "1"
77
+
78
+ def test_profile_copy_bytes(self) -> None:
79
+ print("Running test_profile_copy_bytes")
80
+ rclone = Rclone(_generate_rclone_config())
81
+ sizes = [
82
+ 1024 * 1024 * 1,
83
+ 1024 * 1024 * 2,
84
+ 1024 * 1024 * 4,
85
+ 1024 * 1024 * 8,
86
+ 1024 * 1024 * 16,
87
+ 1024 * 1024 * 32,
88
+ 1024 * 1024 * 64,
89
+ ]
90
+ # transfer_list = [1, 2, 4, 8, 16]
91
+ transfer_list = [1, 2, 4]
92
+
93
+ # src_file = "dst:rclone-api-unit-test/zachs_video/internaly_ai_alignment.mp4"
94
+ # sftp mount
95
+ src_file = "src:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
96
+
97
+ for size in sizes:
98
+ for transfers in transfer_list:
99
+ print("\n\n")
100
+ print("#" * 80)
101
+ print(
102
+ f"# Started test download of {SizeSuffix(size)} with {transfers} transfers"
103
+ )
104
+ print("#" * 80)
105
+ net_io_start = psutil.net_io_counters()
106
+ start = time.time()
107
+ bytes_or_err: bytes | Exception = rclone.copy_bytes(
108
+ src=src_file,
109
+ offset=0,
110
+ length=size,
111
+ direct_io=True,
112
+ transfers=transfers,
113
+ )
114
+ diff = time.time() - start
115
+ net_io_end = psutil.net_io_counters()
116
+ if isinstance(bytes_or_err, Exception):
117
+ print(bytes_or_err)
118
+ self.fail(f"Error: {bytes_or_err}")
119
+ assert isinstance(bytes_or_err, bytes)
120
+ self.assertEqual(len(bytes_or_err), size)
121
+
122
+ # print io stats
123
+ bytes_sent = net_io_end.bytes_sent - net_io_start.bytes_sent
124
+ bytes_recv = net_io_end.bytes_recv - net_io_start.bytes_recv
125
+ packets_sent = net_io_end.packets_sent - net_io_start.packets_sent
126
+ efficiency = size / (bytes_recv)
127
+ efficiency_100 = efficiency * 100
128
+ efficiency_str = f"{efficiency_100:.2f}"
129
+
130
+ bytes_send_suffix = SizeSuffix(bytes_sent)
131
+ bytes_recv_suffix = SizeSuffix(bytes_recv)
132
+ range_size = SizeSuffix(size)
133
+
134
+ print(f"\nFinished downloading {range_size} with {transfers} transfers")
135
+ print("Net IO stats:")
136
+ print(f"Bytes sent: {bytes_send_suffix}")
137
+ print(f"Bytes received: {bytes_recv_suffix}")
138
+ print(f"Packets sent: {packets_sent}")
139
+ print(f"Efficiency: {efficiency_str}%")
140
+ print(f"Time: {diff:.1f} seconds")
141
+
142
+ print("done")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()
rclone_api/rclone.py CHANGED
@@ -80,8 +80,10 @@ class Rclone:
80
80
  ) -> subprocess.CompletedProcess:
81
81
  return self._exec.execute(cmd, check=check, capture=capture)
82
82
 
83
- def _launch_process(self, cmd: list[str], capture: bool | None = None) -> Process:
84
- return self._exec.launch_process(cmd, capture=capture)
83
+ def _launch_process(
84
+ self, cmd: list[str], capture: bool | None = None, log: Path | None = None
85
+ ) -> Process:
86
+ return self._exec.launch_process(cmd, capture=capture, log=log)
85
87
 
86
88
  def webgui(self, other_args: list[str] | None = None) -> Process:
87
89
  """Launch the Rclone web GUI."""
@@ -684,6 +686,7 @@ class Rclone:
684
686
  verbose: bool | None = None,
685
687
  max_chunks_before_suspension: int | None = None,
686
688
  mount_path: Path | None = None,
689
+ mount_log: Path | None = None,
687
690
  ) -> MultiUploadResult:
688
691
  """For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
689
692
  from rclone_api.s3.api import S3Client
@@ -724,6 +727,7 @@ class Rclone:
724
727
  use_links=True,
725
728
  vfs_cache_mode="minimal",
726
729
  verbose=False,
730
+ log=mount_log,
727
731
  other_args=other_args,
728
732
  ):
729
733
  path_info: S3PathInfo = split_s3_path(dst)
@@ -816,10 +820,11 @@ class Rclone:
816
820
  src: str,
817
821
  offset: int,
818
822
  length: int,
819
- transfers: int = 16,
820
- outfile: (
821
- Path | None
822
- ) = None, # If supplied then bytes are written to this file and success returns bytes(0)
823
+ transfers: int = 1, # Note, increasing transfers can result in devestating drop in performance.
824
+ # If outfile is supplied then bytes are written to this file and success returns bytes(0)
825
+ outfile: Path | None = None,
826
+ mount_log: Path | None = None,
827
+ direct_io: bool = True,
823
828
  ) -> bytes | Exception:
824
829
  """Copy bytes from a file to another file."""
825
830
  from rclone_api.util import random_str
@@ -828,8 +833,7 @@ class Rclone:
828
833
  src_parent_path = Path(src).parent.as_posix()
829
834
  src_file = Path(src).name
830
835
  other_args: list[str] = ["--no-modtime", "--vfs-read-wait", "1s"]
831
- unit_chunk_size = length
832
- vfs_read_chunk_size = unit_chunk_size
836
+ vfs_read_chunk_size = length // transfers
833
837
  vfs_read_chunk_size_limit = length
834
838
  vfs_read_chunk_streams = transfers
835
839
  vfs_disk_space_total_size = length
@@ -838,7 +842,8 @@ class Rclone:
838
842
  other_args += ["--vfs-read-chunk-streams", str(vfs_read_chunk_streams)]
839
843
  other_args += ["--vfs-disk-space-total-size", str(vfs_disk_space_total_size)]
840
844
  other_args += ["--read-only"]
841
- other_args += ["--direct-io"]
845
+ if direct_io:
846
+ other_args += ["--direct-io"]
842
847
 
843
848
  try:
844
849
  # use scoped mount to do the read, then write the bytes to the destination
@@ -846,9 +851,10 @@ class Rclone:
846
851
  src_parent_path,
847
852
  tmp_mnt,
848
853
  use_links=True,
849
- verbose=False,
854
+ verbose=mount_log is not None,
850
855
  vfs_cache_mode="minimal",
851
856
  other_args=other_args,
857
+ log=mount_log,
852
858
  ):
853
859
  src_file_mnt = tmp_mnt / src_file
854
860
  with open(src_file_mnt, "rb") as f:
@@ -896,6 +902,7 @@ class Rclone:
896
902
  use_links: bool | None = None,
897
903
  vfs_cache_mode: str | None = None,
898
904
  verbose: bool | None = None,
905
+ log: Path | None = None,
899
906
  other_args: list[str] | None = None,
900
907
  ) -> Mount:
901
908
  """Mount a remote or directory to a local path.
@@ -930,7 +937,7 @@ class Rclone:
930
937
  cmd_list.append("-vvvv")
931
938
  if other_args:
932
939
  cmd_list += other_args
933
- proc = self._launch_process(cmd_list)
940
+ proc = self._launch_process(cmd_list, log=log)
934
941
  mount_read_only = "--read-only" in cmd_list # hint to allow fast teardown
935
942
  mount: Mount = Mount(mount_path=outdir, process=proc, read_only=mount_read_only)
936
943
  return mount
@@ -944,6 +951,7 @@ class Rclone:
944
951
  use_links: bool | None = None,
945
952
  vfs_cache_mode: str | None = None,
946
953
  verbose: bool | None = None,
954
+ log: Path | None = None,
947
955
  other_args: list[str] | None = None,
948
956
  ) -> Generator[Mount, None, None]:
949
957
  """Like mount, but can be used in a context manager."""
@@ -956,6 +964,7 @@ class Rclone:
956
964
  use_links=use_links,
957
965
  vfs_cache_mode=vfs_cache_mode,
958
966
  verbose=verbose,
967
+ log=log,
959
968
  other_args=other_args,
960
969
  )
961
970
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.1.30
3
+ Version: 1.1.32
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -12,6 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: pyright>=1.1.393
13
13
  Requires-Dist: python-dotenv>=1.0.0
14
14
  Requires-Dist: certifi>=2025.1.31
15
+ Requires-Dist: psutil
15
16
  Requires-Dist: boto3<=1.35.99,>=1.20.1
16
17
  Dynamic: home-page
17
18
 
@@ -7,13 +7,13 @@ rclone_api/deprecated.py,sha256=qWKpnZdYcBK7YQZKuVoWWXDwi-uqiAtbjgPcci_efow,590
7
7
  rclone_api/diff.py,sha256=tMoJMAGmLSE6Q_7QhPf6PnCzb840djxMZtDmhc2GlGQ,5227
8
8
  rclone_api/dir.py,sha256=i4h7LX5hB_WmVixxDRWL_l1nifvscrdWct_8Wx7wHZc,3540
9
9
  rclone_api/dir_listing.py,sha256=GoziW8Sne6FY90MLNcb2aO3aaa3jphB6H8ExYrV0Ryo,1882
10
- rclone_api/exec.py,sha256=1ovvaMXDEfLiT7BrYZyE85u_yFhEUwUNW3jPOzqknR8,1023
10
+ rclone_api/exec.py,sha256=Pd7pUBd8ib5MzqvMybG2DQISPRbDRu20VjVRL2mLAVY,1076
11
11
  rclone_api/file.py,sha256=EP5yT2dZ0H2p7CY5n0y5k5pHhIliV25pm8KOwBklUTk,1863
12
12
  rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
13
13
  rclone_api/group_files.py,sha256=H92xPW9lQnbNw5KbtZCl00bD6iRh9yRbCuxku4j_3dg,8036
14
- rclone_api/mount.py,sha256=g9YkKCGJbWvGF7gzSKtWa8pEgphuW0-e0SySxtOirH4,6093
15
- rclone_api/process.py,sha256=Hgn8MGEPkBt8C6C4oIuh-n1t1GkFF2miPlIE1lh_Zbc,5045
16
- rclone_api/rclone.py,sha256=3E7fr4k1bgls_jK2F1qbqz9IxN8SPNt8IdvA4GvY9E4,42229
14
+ rclone_api/mount.py,sha256=ryAjkX4_kFeFZWLiBVpcGy2VilpvVhFbWeWfEX4jMKs,6104
15
+ rclone_api/process.py,sha256=rBj_S86jC6nqCYop-jq8r9eMSteKeObxUrJMgH8LZvI,5084
16
+ rclone_api/rclone.py,sha256=meZkXmIM1M_L0Xu2hf0fcm-tG1cF5t6AHSIM6TOXUQM,42624
17
17
  rclone_api/remote.py,sha256=O9WDUFQy9f6oT1HdUbTixK2eg0xtBBm8k4Xl6aa6K00,431
18
18
  rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
19
19
  rclone_api/scan_missing_folders.py,sha256=Kulca2Q6WZodt00ATFHkmqqInuoPvBkhTcS9703y6po,4740
@@ -25,6 +25,7 @@ rclone_api/cmd/copy_large_s3.py,sha256=-rfedi-ZzPUdCSP8ai9LRL0y1xVkvN-viQQlk8HVU
25
25
  rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
26
26
  rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
27
27
  rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
28
+ rclone_api/profiling/mount_copy_bytes.py,sha256=WfCikFiFhLoZhnTjiKzJkpPWb0dOsfgjfcRtkWoY2Fs,4794
28
29
  rclone_api/s3/api.py,sha256=qxtRDUpHYqJ7StJRtP8U_PbF_BvYRg705568SyvF-R0,3770
29
30
  rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
30
31
  rclone_api/s3/chunk_file.py,sha256=YELR-EzR7RHpzCDGpYdzlwu21NZW5wttIDvLoONI4aU,3477
@@ -32,9 +33,9 @@ rclone_api/s3/chunk_types.py,sha256=LbXayXY1KgVU1LkdbASD_BQ7TpVpwVnzMjtz--8LBaE,
32
33
  rclone_api/s3/create.py,sha256=wgfkapv_j904CfKuWyiBIWJVxfAx_ftemFSUV14aT68,3149
33
34
  rclone_api/s3/types.py,sha256=yBnJ38Tjk6RlydJ-sqZ7DSfyFloy8KDYJ0mv3vlOzLE,1388
34
35
  rclone_api/s3/upload_file_multipart.py,sha256=1jQAdk35Fa9Tcq36bS65262cs7AcNG2DAFQ-NdYlWSw,9961
35
- rclone_api-1.1.30.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
36
- rclone_api-1.1.30.dist-info/METADATA,sha256=-yOOME96u7YXchp4cDxwhmPj5fVD8L6AyxxhQGJIMvU,4514
37
- rclone_api-1.1.30.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
38
- rclone_api-1.1.30.dist-info/entry_points.txt,sha256=6eNqTRXKhVf8CpWNjXiOa_0Du9tHiW_HD2iQSXRsUg8,132
39
- rclone_api-1.1.30.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
40
- rclone_api-1.1.30.dist-info/RECORD,,
36
+ rclone_api-1.1.32.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
37
+ rclone_api-1.1.32.dist-info/METADATA,sha256=ar5X9lipUNxKc2mAzx3wBqIl1jMAyaoVJRicNBs4grY,4537
38
+ rclone_api-1.1.32.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
39
+ rclone_api-1.1.32.dist-info/entry_points.txt,sha256=6eNqTRXKhVf8CpWNjXiOa_0Du9tHiW_HD2iQSXRsUg8,132
40
+ rclone_api-1.1.32.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
41
+ rclone_api-1.1.32.dist-info/RECORD,,